Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions .github/scripts/feishu_release_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@
from dataclasses import dataclass
from pathlib import Path

if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
if sys.stderr.encoding and sys.stderr.encoding.lower() != "utf-8":
try:
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass

STABLE_TAG_PATTERN = re.compile(r"^v\d+\.\d+\.\d+$")
TOPIC_PATTERN = re.compile(r"^\*\*.+? (\d+)\. .+\*\*$", re.MULTILINE)
LINK_PATTERN = re.compile(r"https?://|\[[^\]]+\]\([^)]+\)")
Expand Down Expand Up @@ -125,20 +136,19 @@ class ReleaseEvidence:
code_diff: str


def git_output(*args: str) -> str:
return subprocess.check_output(
["git", *args],
text=True,
stderr=subprocess.DEVNULL,
).strip()
def git_output(*args: str, default: str = "") -> str:
try:
return subprocess.check_output(
["git", *args],
text=True,
stderr=subprocess.DEVNULL,
).strip()
except Exception:
return default


def read_annotated_tag_note(release_tag: str) -> str:
try:
object_type = git_output("cat-file", "-t", f"refs/tags/{release_tag}")
except subprocess.CalledProcessError:
return f"Release {release_tag}"

object_type = git_output("cat-file", "-t", f"refs/tags/{release_tag}")
if object_type != "tag":
return f"Release {release_tag}"

Expand Down
38 changes: 34 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
Expand Down Expand Up @@ -78,12 +80,13 @@ jobs:
env:
CSC_KEYCHAIN: ${{ steps.signing_keychain.outputs.keychain }}
run: npm run package:mac:arm64
- name: Build unsigned Apple Silicon verification package
- name: Build isolated Apple Silicon development package
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
env:
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
run: npm run package:mac:arm64
run: npm run package:dev:mac:arm64
- name: Preserve Apple Silicon update metadata
if: startsWith(github.ref, 'refs/tags/v')
run: mv dist/latest-mac.yml dist/latest-mac-arm64.yml
- name: Sign, notarize, and staple Apple Silicon DMG
if: startsWith(github.ref, 'refs/tags/v')
Expand Down Expand Up @@ -130,6 +133,7 @@ jobs:
spctl --assess --type open --context context:primary-signature --verbose=4 dist/dsh-desktop-mac-arm64.dmg
xcrun stapler validate dist/dsh-desktop-mac-arm64.dmg
- uses: actions/upload-artifact@v4
if: startsWith(github.ref, 'refs/tags/v')
with:
name: macos-apple-silicon
path: |
Expand All @@ -138,13 +142,24 @@ jobs:
dist/dsh-desktop-mac-arm64.zip.blockmap
dist/latest-mac-arm64.yml
if-no-files-found: error
- uses: actions/upload-artifact@v4
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
with:
name: macos-apple-silicon-dev
path: |
dist-dev/dsh-desktop-dev-mac-arm64.dmg
dist-dev/dsh-desktop-dev-mac-arm64.zip
dist-dev/dsh-desktop-dev-mac-arm64.zip.blockmap
if-no-files-found: error

macos-intel:
name: macOS Intel
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
runs-on: macos-15-intel
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
Expand Down Expand Up @@ -196,12 +211,13 @@ jobs:
env:
CSC_KEYCHAIN: ${{ steps.signing_keychain.outputs.keychain }}
run: npm run package:mac:x64
- name: Build unsigned Intel verification package
- name: Build isolated Intel development package
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
env:
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
run: npm run package:mac:x64
run: npm run package:dev:mac:x64
- name: Preserve Intel update metadata
if: startsWith(github.ref, 'refs/tags/v')
run: mv dist/latest-mac.yml dist/latest-mac-x64.yml
- name: Sign, notarize, and staple Intel DMG
if: startsWith(github.ref, 'refs/tags/v')
Expand Down Expand Up @@ -248,6 +264,7 @@ jobs:
spctl --assess --type open --context context:primary-signature --verbose=4 dist/dsh-desktop-mac-x64.dmg
xcrun stapler validate dist/dsh-desktop-mac-x64.dmg
- uses: actions/upload-artifact@v4
if: startsWith(github.ref, 'refs/tags/v')
with:
name: macos-intel
path: |
Expand All @@ -256,13 +273,24 @@ jobs:
dist/dsh-desktop-mac-x64.zip.blockmap
dist/latest-mac-x64.yml
if-no-files-found: error
- uses: actions/upload-artifact@v4
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
with:
name: macos-intel-dev
path: |
dist-dev/dsh-desktop-dev-mac-x64.dmg
dist-dev/dsh-desktop-dev-mac-x64.zip
dist-dev/dsh-desktop-dev-mac-x64.zip.blockmap
if-no-files-found: error

windows-x64:
name: Windows x64
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
Expand Down Expand Up @@ -410,6 +438,8 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
Expand Down
1 change: 1 addition & 0 deletions electron-builder.dev.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
productName: 'DSH Desktop Dev',
dshDesktopChannel: 'development'
},
artifactName: 'dsh-desktop-dev-${os}-${arch}.${ext}',
nsis: {
...packageJson.build.nsis,
artifactName: 'dsh-desktop-dev-windows-${arch}-setup.${ext}'
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"test:watch": "vitest",
"package:dir": "npm run build && electron-builder --dir",
"package:dev:dir": "npm run build && electron-builder --dir --config electron-builder.dev.cjs",
"package:dev:mac:arm64": "node scripts/verify-target.mjs darwin arm64 && npm run build && electron-builder --mac --arm64 --publish never --config electron-builder.dev.cjs",
"package:dev:mac:x64": "node scripts/verify-target.mjs darwin x64 && npm run build && electron-builder --mac --x64 --publish never --config electron-builder.dev.cjs",
"package:dev:win": "node scripts/verify-target.mjs win32 x64 && npm run build && electron-builder --win --x64 --publish never --config electron-builder.dev.cjs",
"package:mac": "npm run build && electron-builder --mac --publish never",
"package:mac:arm64": "node scripts/verify-target.mjs darwin arm64 && npm run build && electron-builder --mac --arm64 --publish never",
Expand Down
34 changes: 34 additions & 0 deletions patches/@deepseek-ai+cordis-plugin-loader+1.0.2.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
diff --git a/node_modules/@deepseek-ai/cordis-plugin-loader/lib/index.js b/node_modules/@deepseek-ai/cordis-plugin-loader/lib/index.js
index 6f4c713..dc48f9d 100644
--- a/node_modules/@deepseek-ai/cordis-plugin-loader/lib/index.js
+++ b/node_modules/@deepseek-ai/cordis-plugin-loader/lib/index.js
@@ -266,10 +266,25 @@ var EntryTree = class EntryTree {
/* @vite-ignore */
new URL(name, this.ctx.baseUrl).href
));
- else return await import(__rewriteRelativeImportExtension(
- /* @vite-ignore */
- name
- ));
+ else {
+ try {
+ return await import(__rewriteRelativeImportExtension(
+ /* @vite-ignore */
+ name
+ ));
+ } catch (error) {
+ if (this.ctx.baseUrl) {
+ try {
+ const { createRequire } = await import("node:module");
+ const { pathToFileURL } = await import("node:url");
+ const req = createRequire(new URL("package.json", this.ctx.baseUrl).href);
+ const resolved = req.resolve(name);
+ return await import(pathToFileURL(resolved).href);
+ } catch {}
+ }
+ throw error;
+ }
+ }
}, getOuterStack);
}
};
7 changes: 6 additions & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import {
Menu,
nativeTheme,
shell,
utilityProcess,
type IpcMainInvokeEvent,
type MessageBoxOptions
} from 'electron'
import { extractFailureCause, HarnessRuntime } from './runtime/harness-runtime'
import { launchDisclaimedUtilityProcess } from './runtime/disclaimed-utility-process'
import { removeProfilePluginWithDsh } from './runtime/profile-plugin-command'
import { LanMobileBridge } from './mobile/lan-mobile-bridge'
import {
Expand Down Expand Up @@ -940,7 +942,10 @@ async function bootstrap(): Promise<void> {
dshPatchPath: desktopResourcePath('dsh-desktop.patch.yml'),
dshHome: join(app.getPath('userData'), 'harness'),
logPath: join(app.getPath('logs'), 'harness.log'),
launchProcess: (executablePath, args, options) => spawn(executablePath, args, options),
launchProcess: (executablePath, args, options) =>
process.platform === 'darwin'
? launchDisclaimedUtilityProcess(utilityProcess, args, options)
: spawn(executablePath, args, options),
onChanged: (snapshot) => {
if (snapshot.phase === 'ready' && snapshot.url) {
void openHarness(snapshot.url).catch(showUnexpectedError)
Expand Down
102 changes: 102 additions & 0 deletions src/main/runtime/disclaimed-utility-process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { EventEmitter } from 'node:events'
import type { SpawnOptionsWithoutStdio } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import type { UtilityProcess } from 'electron'
import type { HarnessChildProcess } from './harness-runtime'

interface UtilityProcessLauncher {
fork(modulePath: string, args?: string[], options?: Electron.ForkOptions): UtilityProcess
}

export interface DisclaimedUtilityProcessSpec {
modulePath: string
args: string[]
options: Electron.ForkOptions
}

export function buildDisclaimedUtilityProcessSpec(
nodeArguments: readonly string[],
spawnOptions: SpawnOptionsWithoutStdio
): DisclaimedUtilityProcessSpec {
const [internalLoaderFlag, modulePath, ...args] = nodeArguments
if (internalLoaderFlag !== '--expose-internals' || !modulePath) {
throw new Error('Unexpected Harness Node arguments for the macOS utility process.')
}

return {
modulePath,
args,
options: {
cwd:
typeof spawnOptions.cwd === 'string'
? spawnOptions.cwd
: spawnOptions.cwd
? fileURLToPath(spawnOptions.cwd)
: undefined,
env: definedEnvironment(spawnOptions.env),
execArgv: [internalLoaderFlag],
stdio: 'pipe',
serviceName: 'DSH Harness',
// Harness loads user-installed plugins and can launch third-party tools.
// Keep their TCC requests out of DSH Desktop's responsibility chain.
disclaim: true
}
}
}

export function launchDisclaimedUtilityProcess(
launcher: UtilityProcessLauncher,
nodeArguments: readonly string[],
spawnOptions: SpawnOptionsWithoutStdio
): HarnessChildProcess {
const spec = buildDisclaimedUtilityProcessSpec(nodeArguments, spawnOptions)
return new UtilityProcessAdapter(
launcher.fork(spec.modulePath, spec.args, spec.options)
)
}

function definedEnvironment(environment: NodeJS.ProcessEnv | undefined): Record<string, string> {
return Object.fromEntries(
Object.entries(environment ?? {}).filter(
(entry): entry is [string, string] => entry[1] !== undefined
)
)
}

class UtilityProcessAdapter extends EventEmitter implements HarnessChildProcess {
readonly stdout: NodeJS.ReadableStream
readonly stderr: NodeJS.ReadableStream
exitCode: number | null = null

constructor(private readonly child: UtilityProcess) {
super()
if (!child.stdout || !child.stderr) {
child.kill()
throw new Error('The DSH Harness utility process did not expose piped output.')
}
this.stdout = child.stdout
this.stderr = child.stderr

child.once('spawn', () => this.emit('spawn'))
child.once('error', (type, location, report) => {
const detail = [type, location, report].filter(Boolean).join(': ')
this.emit('error', new Error(`Harness utility process failed: ${detail}`))
})
child.once('exit', (code) => {
this.exitCode = code
this.emit('exit', code, null)
})
}

kill(signal?: NodeJS.Signals): boolean {
if (signal === 'SIGKILL' && this.child.pid !== undefined) {
try {
process.kill(this.child.pid, signal)
return true
} catch {
return false
}
}
return this.child.kill()
}
}
18 changes: 13 additions & 5 deletions src/main/runtime/harness-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from 'node:child_process'
import type { SpawnOptionsWithoutStdio } from 'node:child_process'
import type { EventEmitter } from 'node:events'
import { createWriteStream, existsSync, type WriteStream } from 'node:fs'
import { mkdir } from 'node:fs/promises'
import { createServer } from 'node:net'
Expand All @@ -16,11 +17,18 @@ export interface HarnessRuntimeOptions {
executablePath: string,
args: string[],
options: SpawnOptionsWithoutStdio
): ChildProcessWithoutNullStreams
): HarnessChildProcess
startupTimeoutMs?: number
onChanged(snapshot: RuntimeSnapshot): void
}

export interface HarnessChildProcess extends EventEmitter {
readonly stdout: NodeJS.ReadableStream
readonly stderr: NodeJS.ReadableStream
readonly exitCode: number | null
kill(signal?: NodeJS.Signals): boolean
}

export function buildHarnessArguments(port: number, patchPath?: string): string[] {
return [
'web',
Expand Down Expand Up @@ -86,7 +94,7 @@ export function updateReadyStability(
}

export class HarnessRuntime {
private child?: ChildProcessWithoutNullStreams
private child?: HarnessChildProcess
private logStream?: WriteStream
private phase: RuntimePhase = 'idle'
private message = 'Harness is not running.'
Expand Down Expand Up @@ -148,7 +156,7 @@ export class HarnessRuntime {
this.writeLog(`[desktop] endpoint ${url}`)
this.setState('starting', 'Starting DeepSeek Harness…')

let child: ChildProcessWithoutNullStreams
let child: HarnessChildProcess
try {
child = this.options.launchProcess(
this.options.nodeExecutablePath,
Expand Down Expand Up @@ -228,7 +236,7 @@ ${cause}`
this.setState('idle', 'Harness is not running.')
}

private async stopChild(child: ChildProcessWithoutNullStreams): Promise<void> {
private async stopChild(child: HarnessChildProcess): Promise<void> {
if (child.exitCode !== null) return
const exitPromise = new Promise<boolean>((resolve) =>
child.once('exit', () => resolve(true))
Expand Down
Loading
Loading