@kingsoftcloud/agentkit-personal-assistant 0.0.0-stage → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +38 -2
  4. package/cordis.patch.yml +6 -0
  5. package/dist/client.js +6 -0
  6. package/package.json +83 -4
  7. package/personal-assistant.plugin.json +31 -0
  8. package/personal-assistant.plugin.schema.json +43 -0
  9. package/runtime/assistant-runtime.mjs +317 -0
  10. package/runtime/check-manifest.mjs +21 -0
  11. package/runtime/cloud-browser-cdp.mjs +313 -0
  12. package/runtime/cloud-browser.mjs +972 -0
  13. package/runtime/cloud-execution.mjs +320 -0
  14. package/runtime/cloud-session-channel.mjs +45 -0
  15. package/runtime/computer-agentd.mjs +488 -0
  16. package/runtime/desktop-clipboard-limit.mjs +2 -0
  17. package/runtime/desktop-launcher.mjs +106 -0
  18. package/runtime/desktop-permit.mjs +42 -0
  19. package/runtime/desktop-session.mjs +840 -0
  20. package/runtime/desktop-ws.mjs +371 -0
  21. package/runtime/prepare-dsh-profile.mjs +17 -0
  22. package/runtime/welcome-server.mjs +144 -0
  23. package/scripts/assistant-runtime-launcher.sh +3 -0
  24. package/scripts/build-artifacts.mjs +81 -0
  25. package/scripts/build-client.mjs +82 -0
  26. package/scripts/computer-agentd-launcher.sh +3 -0
  27. package/scripts/deterministic_tar.py +21 -0
  28. package/scripts/install-local.mjs +262 -0
  29. package/scripts/scan-secrets.mjs +41 -0
  30. package/scripts/sync-public.mjs +52 -0
  31. package/scripts/verification/gateway-desktop-client.mjs +142 -0
  32. package/scripts/verification/verify-desktop-applications-linux.mjs +46 -0
  33. package/scripts/verification/verify-desktop-input-linux.mjs +127 -0
  34. package/scripts/verification/verify-desktop-pod-gateway.mjs +204 -0
  35. package/scripts/verify-cloud-turn.mjs +18 -0
  36. package/scripts/verify-computer-broker.mjs +10 -0
  37. package/src/automation-tools.mjs +18 -0
  38. package/src/bot-private-tools.mjs +23 -0
  39. package/src/bot-registry.mjs +157 -0
  40. package/src/bot-tool-policy.mjs +48 -0
  41. package/src/bot-tool-scope.mjs +40 -0
  42. package/src/client/bot-directory.mjs +23 -0
  43. package/src/client/components/desktop-clipboard.mjs +194 -0
  44. package/src/client/conversation.mjs +558 -0
  45. package/src/client/desktop-control.mjs +70 -0
  46. package/src/client/desktop-view.mjs +68 -0
  47. package/src/client/entry.mjs +8 -0
  48. package/src/client/index.mjs +693 -0
  49. package/src/client/markdown.mjs +130 -0
  50. package/src/client/native-workbench.mjs +62 -0
  51. package/src/cloud-computer-tools.mjs +245 -0
  52. package/src/cloud-files.mjs +57 -0
  53. package/src/cloud-model.mjs +10 -0
  54. package/src/cloud-session-transport.mjs +76 -0
  55. package/src/computer-broker-protocol.mjs +116 -0
  56. package/src/computer-broker.mjs +220 -0
  57. package/src/desktop-tools.mjs +64 -0
  58. package/src/dsh-session-bridge.mjs +207 -0
  59. package/src/dsh-web-bridge.mjs +127 -0
  60. package/src/host-gateway.mjs +228 -0
  61. package/src/host-session-channel.mjs +410 -0
  62. package/src/implementation-capabilities.mjs +10 -0
  63. package/src/index.mjs +148 -0
  64. package/src/persistent-state.mjs +201 -0
  65. package/src/platform-manifest.mjs +24 -0
  66. package/src/plugin-manifest.mjs +82 -0
  67. package/src/schedule-manager.mjs +88 -0
  68. package/src/system-fetch.mjs +72 -0
  69. package/src/workspace-tools.mjs +22 -0
  70. package/src/workspace.mjs +22 -0
@@ -0,0 +1,82 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { build, context } from 'esbuild';
5
+
6
+ /**
7
+ * DSH 客户端插件构建器。
8
+ *
9
+ * 运行入口是 `dist/client.js`(`package.json` 的 `exports['./client']`),
10
+ * 不是 `src/client/*.mjs`;改完源码必须重新构建,Studio 里按 Cmd+R 刷新即可生效,
11
+ * 不需要退出应用(只有 Host 代码、插件树/package.json bundles 变更才需要重启)。
12
+ *
13
+ * 用法:
14
+ * node scripts/build-client.mjs
15
+ * node scripts/build-client.mjs --watch
16
+ * node scripts/build-client.mjs --watch --out-dir ~/.dsh/profiles/<profile>/node_modules/@kingsoftcloud/agentkit-personal-assistant/dist
17
+ */
18
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
19
+ const argv = process.argv.slice(2);
20
+ const watch = argv.includes('--watch');
21
+ const outDirArg = argv.includes('--out-dir') ? argv[argv.indexOf('--out-dir') + 1] : '';
22
+ const outDir = outDirArg ? resolve(outDirArg.replace(/^~(?=$|\/)/u, process.env.HOME || '~')) : resolve(root, 'dist');
23
+
24
+ const stamp = new Date().toISOString();
25
+
26
+ const options = {
27
+ absWorkingDir: root,
28
+ entryPoints: ['src/client/entry.mjs'],
29
+ bundle: true,
30
+ format: 'iife',
31
+ platform: 'browser',
32
+ target: 'es2022',
33
+ minify: true,
34
+ legalComments: 'none',
35
+ write: false,
36
+ metafile: true,
37
+ logLevel: 'silent',
38
+ // 面板可在页脚渲染构建时间,便于确认 Cmd+R 后拿到的是最新产物。
39
+ define: { __PA_CLIENT_BUILD__: JSON.stringify(stamp) },
40
+ };
41
+
42
+ function verifiedOutput(result) {
43
+ const output = result.outputFiles?.[0]?.text || '';
44
+ if (!output) throw new Error('客户端构建没有产物');
45
+ if (!output.includes('window.__ModuleLoader__.load({') || !output.includes('@kingsoftcloud/agentkit-personal-assistant')) throw new Error('DSH 客户端注册入口丢失');
46
+ if (Object.keys(result.metafile.inputs).some(path => !path.startsWith('src/client/'))) throw new Error('浏览器产物含 Host 代码');
47
+ return output;
48
+ }
49
+
50
+ async function emit(result) {
51
+ const output = verifiedOutput(result);
52
+ await mkdir(outDir, { recursive: true });
53
+ await writeFile(resolve(outDir, 'client.js'), output);
54
+ return output;
55
+ }
56
+
57
+ if (watch) {
58
+ const ctx = await context({
59
+ ...options,
60
+ plugins: [{
61
+ name: 'emit-verified-client',
62
+ setup(build) {
63
+ build.onEnd(async result => {
64
+ if (result.errors.length) return;
65
+ try {
66
+ const output = await emit(result);
67
+ const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
68
+ console.log(`[${time}] dist/client.js ${output.length} 字节 → ${outDir}(Studio 里 Cmd+R 刷新)`);
69
+ } catch (error) {
70
+ console.error(`[build:client] ${error.message}`);
71
+ }
72
+ });
73
+ },
74
+ }],
75
+ });
76
+ await ctx.watch();
77
+ console.log(`watching src/client → ${outDir}(改完在 Studio 按 Cmd+R,无需重启应用)`);
78
+ } else {
79
+ const result = await build(options);
80
+ const output = await emit(result);
81
+ console.log(`dist/client.js ${output.length} 字节 → ${outDir}(build ${stamp})`);
82
+ }
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ set -eu
3
+ exec node "$(dirname "$0")/../runtime/computer-agentd.mjs" "$@"
@@ -0,0 +1,21 @@
1
+ import os
2
+ import sys
3
+ import tarfile
4
+
5
+ root, archive = sys.argv[1:]
6
+ with tarfile.open(archive, 'w') as output:
7
+ for base, dirs, files in os.walk(root):
8
+ dirs.sort()
9
+ files.sort()
10
+ for name in dirs + files:
11
+ path = os.path.join(base, name)
12
+ arcname = os.path.relpath(path, root)
13
+ info = output.gettarinfo(path, arcname=arcname)
14
+ info.uid = info.gid = 0
15
+ info.uname = info.gname = ''
16
+ info.mtime = 0
17
+ if info.isfile():
18
+ with open(path, 'rb') as stream:
19
+ output.addfile(info, stream)
20
+ else:
21
+ output.addfile(info)
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ /** 离线安装个人助理、原生侧栏和可选的 Studio 宿主包;默认仅检查。 */
3
+ import { promises as fs } from 'node:fs';
4
+ import { basename, dirname, isAbsolute, join, resolve, posix } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { createHash, randomUUID } from 'node:crypto';
7
+ import { gunzipSync } from 'node:zlib';
8
+ import { spawnSync } from 'node:child_process';
9
+
10
+ const PA = '@kingsoftcloud/agentkit-personal-assistant';
11
+ const SIDEBAR = '@deepseek-ai/dsh-client-ui-sidebar-right';
12
+ const STUDIO = '@kingsoftcloud/agentkit-studio';
13
+ const NODE = '/Applications/AgentKit Studio.app/Contents/MacOS/AgentKit Studio';
14
+ const DSH = '/Applications/AgentKit Studio.app/Contents/Resources/app.asar/dsh/node_modules/@deepseek-ai/dsh/lib/bin.js';
15
+ const sha256 = value => createHash('sha256').update(value).digest('hex');
16
+ const stat = async path => fs.lstat(path).catch(error => { if (error.code === 'ENOENT') return null; throw error; });
17
+ const fail = message => { throw new Error(message); };
18
+ function parseJson(bytes, label) {
19
+ try { return JSON.parse(bytes.toString()); } catch (error) { fail(label + ' 不是有效 JSON;不打印原文'); }
20
+ }
21
+
22
+ function readTar(archive) {
23
+ const tar = gunzipSync(archive, { maxOutputLength: 200 * 1024 * 1024 });
24
+ const files = [], paths = new Set();
25
+ let offset = 0, unpacked = 0, pax = null;
26
+ const field = (header, start, length) => header.subarray(start, start + length).toString().replace(/\0.*$/s, '');
27
+ while (offset + 512 <= tar.length) {
28
+ const header = tar.subarray(offset, offset + 512); offset += 512;
29
+ if (header.every(byte => byte === 0)) break;
30
+ const checksum = Number.parseInt(field(header, 148, 8).trim(), 8);
31
+ const actual = header.reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
32
+ if (checksum !== actual) fail('tar 头校验失败');
33
+ const sizeText = field(header, 124, 12).trim();
34
+ if (!/^[0-7]+$/.test(sizeText)) fail('tar 文件长度无效');
35
+ const size = Number.parseInt(sizeText, 8), type = field(header, 156, 1);
36
+ if (offset + size > tar.length || !Number.isSafeInteger(size)) fail('tar 文件被截断');
37
+ if (type === 'x') {
38
+ if (pax) fail('tar PAX 扩展头不能重叠');
39
+ pax = Object.create(null);
40
+ const data = tar.subarray(offset, offset + size);
41
+ for (let cursor = 0; cursor < data.length;) {
42
+ const space = data.indexOf(32, cursor), lengthText = data.subarray(cursor, space).toString();
43
+ if (space < cursor || !/^[1-9][0-9]*$/.test(lengthText)) fail('tar PAX 长度无效');
44
+ const end = cursor + Number(lengthText);
45
+ if (!Number.isSafeInteger(end) || end > data.length || end <= space + 2 || data[end - 1] !== 10) fail('tar PAX 内容无效');
46
+ const record = data.subarray(space + 1, end - 1).toString(), equal = record.indexOf('='), key = record.slice(0, equal);
47
+ if (equal < 1 || !['path', 'size', 'mtime', 'atime', 'ctime', 'uid', 'gid', 'uname', 'gname'].includes(key) || Object.hasOwn(pax, key)) fail('tar PAX 字段不安全');
48
+ pax[key] = record.slice(equal + 1); cursor = end;
49
+ }
50
+ offset += Math.ceil(size / 512) * 512; continue;
51
+ }
52
+ if (!['', '0', '5'].includes(type)) fail('tar 不允许链接或特殊文件');
53
+ if (pax?.size !== undefined && (!/^[0-9]+$/.test(pax.size) || Number(pax.size) !== size)) fail('tar PAX 文件长度不一致');
54
+ const prefix = field(header, 345, 155), name = pax?.path ?? ((prefix ? prefix + '/' : '') + field(header, 0, 100));
55
+ pax = null;
56
+ const normalized = name.replace(/\/$/, '');
57
+ if (!normalized.startsWith('package/') || normalized.includes('\\') || normalized.includes('\n') ||
58
+ posix.normalize(normalized) !== normalized || normalized.split('/').some(part => part === '..' || part === '.')) {
59
+ fail('tar 成员路径不安全');
60
+ }
61
+ if (paths.has(normalized)) fail('tar 成员路径重复');
62
+ paths.add(normalized);
63
+ const path = normalized.slice('package/'.length);
64
+ unpacked += size;
65
+ if (unpacked > 200 * 1024 * 1024) fail('tar 解包大小超限');
66
+ files.push({ path, directory: type === '5', mode: Number.parseInt(field(header, 100, 8).trim(), 8) & 0o777,
67
+ data: tar.subarray(offset, offset + size) });
68
+ offset += Math.ceil(size / 512) * 512;
69
+ }
70
+ if (pax) fail('tar PAX 扩展头缺少后续文件');
71
+ // 写入前检查文件/目录冲突,避免归档顺序改变路径解析。
72
+ const regular = new Set(files.filter(file => !file.directory).map(file => file.path));
73
+ for (const file of files) for (let parent = posix.dirname(file.path); parent !== '.'; parent = posix.dirname(parent)) {
74
+ if (regular.has(parent)) fail('tar 文件与目录冲突');
75
+ }
76
+ return files;
77
+ }
78
+
79
+ async function requireDirectory(path, description, optional = false) {
80
+ const info = await stat(path);
81
+ if (!info && optional) return false;
82
+ if (!info?.isDirectory() || info.isSymbolicLink()) fail(description + '必须为真实目录,拒绝链接');
83
+ return true;
84
+ }
85
+
86
+ async function extract(files, directory) {
87
+ await fs.mkdir(directory, { mode: 0o700 });
88
+ for (const file of files) {
89
+ const target = join(directory, file.path);
90
+ await fs.mkdir(file.directory ? target : dirname(target), { recursive: true });
91
+ if (!file.directory) await fs.writeFile(target, file.data, { flag: 'wx', mode: file.mode || 0o644 });
92
+ }
93
+ }
94
+
95
+ export async function installLocal({ profile, manifest, apply = false, dshNode = NODE, dshBin = DSH }) {
96
+ if (!isAbsolute(profile || '')) fail('--profile 必须是绝对路径');
97
+ profile = resolve(profile);
98
+ if (basename(dirname(profile)) !== 'profiles' || !basename(profile)) fail('目标必须是 <DSH_HOME>/profiles/<名称>');
99
+ await requireDirectory(profile, 'profile');
100
+ await requireDirectory(dirname(profile), 'profiles');
101
+ await requireDirectory(dirname(dirname(profile)), 'DSH_HOME');
102
+ const packagePath = join(profile, 'package.json');
103
+ const packageInfo = await stat(packagePath);
104
+ if (!packageInfo?.isFile()) fail('profile package.json 不存在或为链接');
105
+ const original = await fs.readFile(packagePath), pkg = parseJson(original, 'profile package.json');
106
+ const manifestPath = resolve(manifest), manifestBytes = await fs.readFile(manifestPath);
107
+ const release = parseJson(manifestBytes, 'release manifest');
108
+ if (!Array.isArray(release.packages) || ![2, 3].includes(release.packages.length)) fail('manifest 必须包含个人助理、原生侧栏和可选的 Studio 包');
109
+ const names = release.packages.map(item => item.pkgName);
110
+ if (new Set(names).size !== names.length || !names.includes(PA) || !names.includes(SIDEBAR) ||
111
+ names.some(name => ![PA, SIDEBAR, STUDIO].includes(name))) fail('manifest 包集合不正确');
112
+ const sourceCommit = release.sidebar?.sourceCommit || release.packages.find(item => item.pkgName === SIDEBAR).sourceCommit;
113
+ if (!/^[a-f0-9]{40}$/.test(sourceCommit || '')) fail('manifest 缺少侧栏 sourceCommit');
114
+ const prepared = [];
115
+ for (const item of release.packages) {
116
+ if (!item.version || typeof item.version !== 'string' || !/^[a-f0-9]{64}$/.test(item.sha256 || '') ||
117
+ !item.filename || basename(item.filename) !== item.filename || item.filename.includes('\\')) fail('包版本、文件名或 SHA256 无效');
118
+ const source = join(dirname(manifestPath), item.filename);
119
+ if (!(await stat(source))?.isFile()) fail('安装包不存在或为链接:' + item.filename);
120
+ const archive = await fs.readFile(source);
121
+ if (sha256(archive) !== item.sha256) fail('安装包 SHA256 不匹配:' + item.pkgName);
122
+ const files = readTar(archive), metaFile = files.find(file => file.path === 'package.json' && !file.directory);
123
+ const meta = metaFile && parseJson(metaFile.data, '归档 package.json');
124
+ if (meta?.name !== item.pkgName || meta?.version !== item.version) fail('归档包名或版本不匹配:' + item.pkgName);
125
+ const clientPath = item.pkgName === SIDEBAR ? 'lib/client.js' : 'dist/client.js';
126
+ const client = files.find(file => file.path === clientPath && !file.directory);
127
+ if (!client?.data.length || (item.pkgName === SIDEBAR && !client.data.includes(Buffer.from('registerPanelSurface')))) fail('客户端产物缺失:' + item.pkgName);
128
+ const [scope, leaf] = item.pkgName.split('/');
129
+ prepared.push({ item, source, archive, files, scope, leaf });
130
+ }
131
+ const modules = join(profile, 'node_modules'), cache = join(profile, '.pa-artifacts'), backups = join(profile, '.pa-backups');
132
+ const modulesExisted = await requireDirectory(modules, 'node_modules', true);
133
+ const cacheExisted = await requireDirectory(cache, '.pa-artifacts', true);
134
+ await requireDirectory(backups, '.pa-backups', true);
135
+ for (const entry of prepared) {
136
+ const scopeInfo = await stat(join(modules, entry.scope));
137
+ if (scopeInfo && !scopeInfo.isDirectory() && !scopeInfo.isSymbolicLink()) fail('包命名空间不是目录');
138
+ if (scopeInfo?.isSymbolicLink()) await fs.realpath(join(modules, entry.scope));
139
+ entry.namespaceIsLink = scopeInfo?.isSymbolicLink() || false;
140
+ entry.cacheName = entry.item.sha256 + '-' + entry.item.filename;
141
+ const cachePath = join(cache, entry.cacheName), cached = await stat(cachePath);
142
+ if (cached && (!cached.isFile() || sha256(await fs.readFile(cachePath)) !== entry.item.sha256)) fail('制品缓存存在冲突');
143
+ }
144
+ if (pkg.dependencies != null && (typeof pkg.dependencies !== 'object' || Array.isArray(pkg.dependencies))) fail('profile dependencies 格式无效');
145
+ if (pkg.dsh != null && (typeof pkg.dsh !== 'object' || Array.isArray(pkg.dsh))) fail('profile dsh 格式无效');
146
+ if (pkg.dsh?.profile != null && (typeof pkg.dsh.profile !== 'object' || Array.isArray(pkg.dsh.profile))) fail('profile dsh.profile 格式无效');
147
+ if (pkg.dsh?.profile?.bundles != null && !Array.isArray(pkg.dsh.profile.bundles)) fail('profile bundles 格式无效');
148
+ const plan = { dryRun: !apply, profile, manifestSha256: sha256(manifestBytes), sidebarSourceCommit: sourceCommit,
149
+ packages: prepared.map(({ item, namespaceIsLink }) => ({ ...item, namespaceIsLink })),
150
+ credentialsChanged: false, cloudBindingCreated: false, requiresUserRestart: true, appReplaced: false };
151
+ if (!apply) return plan;
152
+
153
+ const lock = join(profile, '.pa-install.lock');
154
+ const handle = await fs.open(lock, 'wx', 0o600).catch(error => { if (error.code === 'EEXIST') fail('该 profile 已有安装锁,请确认没有并发安装'); throw error; });
155
+ await handle.close();
156
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-') + '-' + randomUUID().slice(0, 8);
157
+ const backup = join(backups, stamp + '-personal-assistant'), stage = join(profile, '.pa-stage-' + stamp);
158
+ const changed = [], newCache = [], configNames = ['cordis.patch.yml', 'cordis.yml'], configModes = new Map();
159
+ let packageChanged = false, setupStarted = false;
160
+ try {
161
+ await fs.mkdir(backups, { recursive: true, mode: 0o700 });
162
+ await fs.mkdir(backup, { mode: 0o700 });
163
+ await fs.writeFile(join(backup, 'package.json'), original, { mode: 0o600 });
164
+ for (const name of configNames) {
165
+ const info = await stat(join(profile, name));
166
+ if (info) {
167
+ await fs.cp(join(profile, name), join(backup, name), { dereference: false, verbatimSymlinks: true });
168
+ if (!info.isSymbolicLink()) await fs.chmod(join(backup, name), 0o600);
169
+ }
170
+ configModes.set(name, info?.mode);
171
+ }
172
+ await fs.mkdir(stage, { mode: 0o700 });
173
+ for (const entry of prepared) await extract(entry.files, join(stage, entry.leaf));
174
+ setupStarted = true;
175
+ await fs.mkdir(modules, { recursive: true });
176
+ await fs.mkdir(cache, { recursive: true, mode: 0o700 });
177
+ for (const entry of prepared) {
178
+ const namespace = join(modules, entry.scope), target = join(namespace, entry.leaf);
179
+ const record = { namespace, target, namespaceBackup: join(backup, entry.scope + '.original'),
180
+ packageBackup: join(backup, entry.leaf + '.original'), namespaceCreated: false, namespaceMoved: false, packageMoved: false, installed: false };
181
+ changed.push(record);
182
+ const scopeInfo = await stat(namespace);
183
+ if (scopeInfo?.isSymbolicLink()) {
184
+ const oldRoot = await fs.realpath(namespace), children = await fs.readdir(oldRoot);
185
+ await fs.rename(namespace, record.namespaceBackup); record.namespaceMoved = true;
186
+ await fs.mkdir(namespace); record.namespaceCreated = true;
187
+ for (const child of children) if (child !== entry.leaf) await fs.symlink(join(oldRoot, child), join(namespace, child));
188
+ } else {
189
+ if (scopeInfo && !scopeInfo.isDirectory()) fail('包命名空间在安装过程中被替换');
190
+ if (!scopeInfo) { await fs.mkdir(namespace); record.namespaceCreated = true; }
191
+ if (await stat(target)) { await fs.rename(target, record.packageBackup); record.packageMoved = true; }
192
+ }
193
+ await fs.rename(join(stage, entry.leaf), target); record.installed = true;
194
+ await fs.writeFile(join(target, 'agentkit-local-release.json'), JSON.stringify({ ...entry.item, sidebarSourceCommit: sourceCommit }, null, 2) + '\n');
195
+ const cached = join(cache, entry.cacheName);
196
+ if (!await stat(cached)) { await fs.writeFile(cached, entry.archive, { flag: 'wx', mode: 0o600 }); newCache.push(cached); }
197
+ pkg.dependencies ??= {};
198
+ pkg.dependencies[entry.item.pkgName] = 'file:./.pa-artifacts/' + entry.cacheName;
199
+ }
200
+ pkg.dsh ??= {}; pkg.dsh.profile ??= {}; pkg.dsh.profile.bundles ??= [];
201
+ if (!pkg.dsh.profile.bundles.includes(PA)) pkg.dsh.profile.bundles.push(PA);
202
+ const pending = join(stage, 'package.json');
203
+ await fs.writeFile(pending, JSON.stringify(pkg, null, 2) + '\n', { mode: packageInfo.mode & 0o777 });
204
+ await fs.rename(pending, packagePath); packageChanged = true;
205
+ const dump = spawnSync(dshNode, [dshBin, '--profile', basename(profile), '--dump-config'], {
206
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', DSH_HOME: dirname(dirname(profile)) },
207
+ timeout: 60000, maxBuffer: 32 * 1024 * 1024, encoding: 'utf8' });
208
+ if (dump.error || dump.status !== 0) fail('DSH 配置合成失败,退出码 ' + String(dump.status ?? dump.error?.code ?? 'unknown') + ';输出可能含凭据,不打印');
209
+ const proof = { ...plan, dryRun: false, backup, dumpConfigExit: 0, dumpConfigLines: dump.stdout.split('\n').length,
210
+ dumpConfigWarningLines: dump.stderr.split('\n').filter(Boolean).length, artifactCache: cache, studioRestarted: false };
211
+ await fs.writeFile(join(backup, 'install-proof.json'), JSON.stringify(proof, null, 2) + '\n', { mode: 0o600 });
212
+ return proof;
213
+ } catch (error) {
214
+ const rollbackErrors = [];
215
+ const restore = async action => { try { await action(); } catch (rollbackError) { rollbackErrors.push(rollbackError.code || 'RESTORE_FAILED'); } };
216
+ for (const record of changed.reverse()) {
217
+ if (record.installed) await restore(() => fs.rm(record.target, { recursive: true, force: true }));
218
+ if (record.namespaceMoved) {
219
+ await restore(() => fs.rm(record.namespace, { recursive: true, force: true }));
220
+ await restore(() => fs.rename(record.namespaceBackup, record.namespace));
221
+ } else {
222
+ if (record.packageMoved) await restore(() => fs.rename(record.packageBackup, record.target));
223
+ if (record.namespaceCreated) await restore(() => fs.rmdir(record.namespace));
224
+ }
225
+ }
226
+ if (packageChanged) await restore(async () => {
227
+ const pending = join(profile, '.pa-package-restore-' + stamp);
228
+ await fs.writeFile(pending, original, { mode: packageInfo.mode & 0o777 }); await fs.rename(pending, packagePath);
229
+ });
230
+ for (const name of configNames) if (configModes.has(name)) await restore(async () => {
231
+ await fs.rm(join(profile, name), { force: true });
232
+ if (configModes.get(name) !== undefined) {
233
+ await fs.cp(join(backup, name), join(profile, name), { dereference: false, verbatimSymlinks: true });
234
+ if (!(await stat(join(profile, name))).isSymbolicLink()) await fs.chmod(join(profile, name), configModes.get(name) & 0o777);
235
+ }
236
+ });
237
+ for (const file of newCache) await restore(() => fs.rm(file, { force: true }));
238
+ if (setupStarted && !cacheExisted) await restore(() => fs.rmdir(cache));
239
+ if (setupStarted && !modulesExisted) await restore(() => fs.rmdir(modules));
240
+ error.installResult = { rolledBack: rollbackErrors.length === 0, rollbackErrors, backup, studioRestarted: false };
241
+ throw error;
242
+ } finally {
243
+ await fs.rm(stage, { recursive: true, force: true });
244
+ await fs.rm(lock, { force: true });
245
+ }
246
+ }
247
+
248
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
249
+ try {
250
+ const args = process.argv.slice(2), options = {};
251
+ for (let i = 0; i < args.length; i++) {
252
+ if (args[i] === '--apply') { options.apply = true; continue; }
253
+ const key = { '--profile': 'profile', '--manifest': 'manifest', '--dsh-node': 'dshNode', '--dsh-bin': 'dshBin' }[args[i]];
254
+ if (!key || !args[i + 1] || args[i + 1].startsWith('--')) fail('参数无效;用法:node install-local.mjs --profile <绝对路径> --manifest <release-manifest.json> [--apply]');
255
+ if (options[key] !== undefined) fail('参数重复'); options[key] = args[++i];
256
+ }
257
+ if (!options.manifest) fail('缺少 --manifest');
258
+ console.log(JSON.stringify(await installLocal(options), null, 2));
259
+ } catch (error) {
260
+ console.error(JSON.stringify({ error: error.message, ...error.installResult })); process.exitCode = 1;
261
+ }
262
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // 轻量密钥扫描:公开仓库 CI 用,检测 AK/SK、令牌、私钥和高危赋值。
3
+ // 只做行级启发式检查;发现命中即失败,避免把凭证带进公开镜像。
4
+ import { execFileSync } from 'node:child_process';
5
+ import { readFileSync } from 'node:fs';
6
+
7
+ const ROOT = new URL('..', import.meta.url).pathname;
8
+
9
+ const RULES = [
10
+ { name: '私钥块', pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/u },
11
+ { name: 'AWS/金山云 AccessKey', pattern: /\bAKID[0-9A-Za-z]{16,}|\bAKIA[0-9A-Z]{16}\b|\bAKLT[0-9A-Za-z-]{10,}\b/u },
12
+ { name: '疑似 SecretKey 赋值', pattern: /(?:secret[_-]?key|access[_-]?key[_-]?secret)\s*[:=]\s*['"][A-Za-z0-9/+=]{24,}['"]/iu },
13
+ { name: 'npm token', pattern: /npm_[A-Za-z0-9]{36,}/u },
14
+ { name: 'GitHub token', pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/u },
15
+ { name: 'Bearer 硬编码', pattern: /Bearer\s+[A-Za-z0-9._~+/-]{32,}/u },
16
+ ];
17
+
18
+ const files = execFileSync('git', ['ls-files'], { cwd: ROOT, encoding: 'utf8' }).trim().split('\n')
19
+ .filter(file => file && !/^(?:node_modules|\.artifacts)\//u.test(file));
20
+
21
+ const hits = [];
22
+ for (const file of files) {
23
+ let text;
24
+ try { text = readFileSync(ROOT + '/' + file, 'utf8'); }
25
+ catch { continue; }
26
+ // 测试夹具里允许出现假令牌(只保留私钥规则),避免把 mock 当成凭证。
27
+ const fixture = /^(?:test|\.artifacts)\//u.test(file);
28
+ text.split('\n').forEach((line, index) => {
29
+ for (const rule of RULES) {
30
+ if (fixture && rule.name !== '私钥块') continue;
31
+ if (rule.pattern.test(line)) hits.push(`${file}:${index + 1} [${rule.name}]`);
32
+ }
33
+ });
34
+ }
35
+
36
+ if (hits.length) {
37
+ console.error(`发现 ${hits.length} 处疑似密钥:`);
38
+ for (const hit of hits.slice(0, 50)) console.error(' ' + hit);
39
+ process.exit(1);
40
+ }
41
+ console.log(`密钥扫描通过:${files.length} 个文件,未发现疑似凭证`);
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ // 把当前提交导出成面向 GitHub 的公开快照:
3
+ // - 排除内部文档 docs/、本地产物 .artifacts/;不携带内部提交历史(单条初始提交)
4
+ // - 公开仓库只作为发布镜像,主仓仍在 ezone
5
+ // 用法:
6
+ // node scripts/sync-public.mjs --remote git@github.com:kingsoftcloud/agentkit-personal-assistant.git [--apply]
7
+ import { execFileSync } from 'node:child_process';
8
+ import { mkdtempSync, rmSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join, resolve } from 'node:path';
11
+
12
+ const root = resolve(import.meta.dirname, '..');
13
+ const args = process.argv.slice(2);
14
+ const apply = args.includes('--apply');
15
+ const remote = args.includes('--remote') ? args[args.indexOf('--remote') + 1] : '';
16
+ const EXCLUDE = ['docs/', '.artifacts/', '.worktrees/', 'output/'];
17
+
18
+ const run = (file, args = []) => execFileSync(file, args, { cwd: root, stdio: 'pipe' }).toString();
19
+
20
+ const dirty = run('git', ['status', '--porcelain']).trim();
21
+ if (dirty) throw new Error('工作区不干净,先提交再同步:\n' + dirty);
22
+
23
+ const files = run('git', ['ls-files']).trim().split('\n')
24
+ .filter(file => file && !EXCLUDE.some(prefix => file.startsWith(prefix)));
25
+ if (!files.length) throw new Error('没有可导出的文件');
26
+
27
+ const staging = mkdtempSync(join(tmpdir(), 'pa-public-'));
28
+ try {
29
+ // 用 checkout-index 一次性导出当前索引(即 HEAD 的已跟踪文件)。
30
+ execFileSync('git', ['checkout-index', '-a', '-f', `--prefix=${staging}/`], { cwd: root, stdio: 'inherit' });
31
+ for (const prefix of EXCLUDE) rmSync(join(staging, prefix), { recursive: true, force: true });
32
+
33
+ const version = JSON.parse(run('git', ['show', 'HEAD:package.json'])).version;
34
+ const git = (...rest) => execFileSync('git', rest, { cwd: staging, stdio: 'pipe' });
35
+ git('init', '-q', '-b', 'main');
36
+ git('add', '-A');
37
+ git('-c', 'user.name=AgentKit', '-c', 'user.email=agentkit@kingsoft.cloud',
38
+ 'commit', '-q', '-m', `AgentKit personal assistant ${version}`);
39
+ const head = git('rev-parse', '--short', 'HEAD').toString().trim();
40
+ console.log(`公开快照已生成:${staging}(${files.length} 个文件,提交 ${head})`);
41
+ if (!apply) {
42
+ console.log('未传 --apply,未推送。');
43
+ process.exit(0);
44
+ }
45
+ if (!remote) throw new Error('--apply 需要同时提供 --remote <git url>');
46
+ git('remote', 'add', 'origin', remote);
47
+ git('push', '--force', 'origin', 'main');
48
+ console.log(`已推送到 ${remote} main`);
49
+ }
50
+ finally {
51
+ if (!apply) rmSync(staging, { recursive: true, force: true });
52
+ }
@@ -0,0 +1,142 @@
1
+ /** 验收专用 Gateway WSS 客户端:TLS 保持验证,票据只放子协议,JPEG 只计数。 */
2
+ import https from 'node:https';
3
+ import { createHash, randomBytes } from 'node:crypto';
4
+ import { EventEmitter } from 'node:events';
5
+
6
+ const PROTOCOL = 'agentkit.personal-assistant.desktop.v1', MAX_FRAME = 400 * 1024;
7
+ const maskedFrame = (opcode, data = Buffer.alloc(0)) => {
8
+ const body = Buffer.isBuffer(data) ? data : Buffer.from(data), mask = randomBytes(4);
9
+ const header = Buffer.alloc(body.length < 126 ? 2 : 4);
10
+ header[0] = 0x80 | opcode;
11
+ if (body.length < 126) header[1] = 0x80 | body.length;
12
+ else { header[1] = 0x80 | 126; header.writeUInt16BE(body.length, 2); }
13
+ const payload = Buffer.from(body);
14
+ for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i % 4];
15
+ return Buffer.concat([header, mask, payload]);
16
+ };
17
+
18
+ export class GatewayDesktopClient extends EventEmitter {
19
+ constructor(socket, { openedAt = Date.now() } = {}) {
20
+ super(); this.socket = socket; this.openedAt = openedAt; this.events = []; this.sequence = 0;
21
+ this.frames = []; this.pending = Buffer.alloc(0); this.fragment = null; this.controlEpoch = 0; this.closed = false;
22
+ socket.on('data', chunk => { try { this.parse(chunk); } catch (error) { this.failure = error; socket.destroy(); this.emit('ended'); } });
23
+ socket.on('error', error => { this.failure = error; this.emit('ended'); });
24
+ socket.on('close', () => { this.closed = true; this.emit('ended'); });
25
+ }
26
+ parse(chunk) {
27
+ this.pending = Buffer.concat([this.pending, chunk]);
28
+ if (this.pending.length > 2 * 1024 * 1024) throw new Error('gateway WS receive buffer exceeds limit');
29
+ while (this.pending.length >= 2) {
30
+ const fin = Boolean(this.pending[0] & 0x80), opcode = this.pending[0] & 15;
31
+ if (this.pending[0] & 0x70) throw new Error('unexpected WS extension bits');
32
+ if (this.pending[1] & 0x80) throw new Error('server WS frames must not be masked');
33
+ let length = this.pending[1] & 127, offset = 2;
34
+ if (length === 126) { if (this.pending.length < 4) return; length = this.pending.readUInt16BE(2); offset = 4; }
35
+ if (length === 127) {
36
+ if (this.pending.length < 10) return;
37
+ const wide = this.pending.readBigUInt64BE(2);
38
+ if (wide > BigInt(MAX_FRAME)) throw new Error('gateway WS frame exceeds limit');
39
+ length = Number(wide); offset = 10;
40
+ }
41
+ if (length > MAX_FRAME) throw new Error('gateway WS frame exceeds limit');
42
+ if (this.pending.length < offset + length) return;
43
+ const payload = this.pending.subarray(offset, offset + length); this.pending = this.pending.subarray(offset + length);
44
+ if (opcode === 8) { this.closed = true; this.emit('ended'); return; }
45
+ if (opcode === 9) { this.socket.write(maskedFrame(10, payload)); continue; }
46
+ if (opcode === 10) continue;
47
+ if (opcode === 0) {
48
+ if (!this.fragment) throw new Error('unexpected WS continuation');
49
+ this.fragment.chunks.push(payload); this.fragment.bytes += length;
50
+ if (this.fragment.bytes > MAX_FRAME) throw new Error('fragmented WS message exceeds limit');
51
+ if (fin) { const fragment = this.fragment; this.fragment = null; this.message(fragment.opcode, Buffer.concat(fragment.chunks)); }
52
+ } else if (!fin) {
53
+ if (this.fragment || ![1, 2].includes(opcode)) throw new Error('invalid fragmented WS message');
54
+ this.fragment = { opcode, chunks: [payload], bytes: payload.length };
55
+ } else this.message(opcode, payload);
56
+ }
57
+ }
58
+ message(opcode, payload) {
59
+ if (opcode === 2) {
60
+ if (payload.length > 307200) throw new Error('JPEG exceeds 300 KiB');
61
+ if (payload.length < 4 || payload.readUInt16BE(0) !== 0xffd8 || payload.readUInt16BE(payload.length - 2) !== 0xffd9) throw new Error('gateway binary payload is not JPEG');
62
+ this.frames.push({ at: Date.now(), bytes: payload.length });
63
+ if (this.frames.length > 3000) this.frames.shift();
64
+ this.emit('frame'); return;
65
+ }
66
+ if (opcode !== 1) throw new Error('unsupported gateway WS opcode');
67
+ const value = JSON.parse(payload.toString('utf8'));
68
+ if (value.type === 'frame') return; // 不保留画面、base64 或帧正文。
69
+ if (['ready', 'control'].includes(value.type)) {
70
+ if (Number.isSafeInteger(value.controlEpoch)) this.controlEpoch = value.controlEpoch;
71
+ this.readOnly = value.readOnly; this.sessionId = value.sessionId || this.sessionId;
72
+ }
73
+ this.events.push({ sequence: ++this.sequence, value });
74
+ if (this.events.length > 256) this.events.shift();
75
+ this.emit('message');
76
+ }
77
+ send(value) { if (this.closed) throw this.failure || new Error('gateway WS is closed'); this.socket.write(maskedFrame(1, JSON.stringify(value))); }
78
+ next(predicate, { after = 0, timeoutMs = 12000 } = {}) {
79
+ return new Promise((resolve, reject) => {
80
+ let timer;
81
+ const finish = (error, value) => { clearTimeout(timer); this.off('message', inspect); this.off('ended', ended); error ? reject(error) : resolve(value); };
82
+ const inspect = () => {
83
+ const index = this.events.findIndex(event => event.sequence > after && predicate(event.value));
84
+ if (index >= 0) finish(null, this.events.splice(index, 1)[0].value);
85
+ };
86
+ const ended = () => finish(this.failure || new Error('gateway WS ended before response'));
87
+ timer = setTimeout(() => finish(new Error('gateway WS response timeout')), timeoutMs);
88
+ this.on('message', inspect); this.on('ended', ended); inspect();
89
+ if (this.closed) ended();
90
+ });
91
+ }
92
+ async request(value, predicate) {
93
+ const after = this.sequence; this.send(value);
94
+ const reply = await this.next(event => event.type === 'error' || predicate(event), { after });
95
+ if (reply.type === 'error') throw Object.assign(new Error(reply.message || reply.code), { code: reply.code, status: reply.status });
96
+ return reply;
97
+ }
98
+ async close() {
99
+ if (this.closed) return;
100
+ const done = new Promise(resolve => { const timer = setTimeout(() => { this.socket.destroy(); resolve(); }, 3000); this.socket.once('close', () => { clearTimeout(timer); resolve(); }); });
101
+ this.send({ type: 'close' }); this.socket.end(maskedFrame(8, Buffer.from([0x03, 0xe8])));
102
+ await done;
103
+ }
104
+ }
105
+
106
+ export async function openGatewayDesktop({ gatewayUrl, apiKey, agentId, botId, mode = 'control', taskId }) {
107
+ const gateway = new URL(gatewayUrl);
108
+ if (gateway.protocol !== 'https:' || gateway.username || gateway.password || gateway.search || gateway.hash || gateway.pathname !== '/') throw new Error('verification requires an HTTPS gateway origin');
109
+ const began = Date.now();
110
+ const issued = await fetch(new URL('IssuePersonalAssistantDesktopTicket', gateway), {
111
+ method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10000),
112
+ headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
113
+ body: JSON.stringify({ AgentId: agentId, BotId: botId, Mode: mode }),
114
+ });
115
+ const result = await issued.json();
116
+ if (!issued.ok || result.Code !== 0) throw Object.assign(new Error(`gateway ticket refused: HTTP ${issued.status}; ${String(result.Message || 'unknown').slice(0, 200)}`), { status: issued.status });
117
+ const data = result.Data;
118
+ // Server's public ticket contract is an integer Unix timestamp in seconds.
119
+ // Do not accept date strings or milliseconds: either can mask a bad fixture.
120
+ const expiresAt = Number.isSafeInteger(data?.ExpiresAt) ? data.ExpiresAt * 1000 : NaN;
121
+ if (!/^[A-Za-z0-9_-]{32,512}$/u.test(data?.Ticket || '') || data.WsPath !== 'personal-assistant/desktop/ws' || data.AgentId !== agentId || data.BotId !== botId
122
+ || !Number.isFinite(expiresAt) || expiresAt <= Date.now() || expiresAt - Date.now() > 60000) throw new Error('gateway ticket binding is invalid');
123
+ const url = new URL(data.WsPath, gateway), key = randomBytes(16).toString('base64');
124
+ const expected = createHash('sha1').update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest('base64');
125
+ const openedAt = Date.now();
126
+ const client = await new Promise((resolve, reject) => {
127
+ const request = https.request(url, { headers: { connection: 'Upgrade', upgrade: 'websocket', 'sec-websocket-version': '13', 'sec-websocket-key': key,
128
+ 'sec-websocket-protocol': `${PROTOCOL}, agentkit.desktop.ticket.${data.Ticket}` }, timeout: 10000 });
129
+ request.on('upgrade', (response, socket, head) => {
130
+ if (response.headers['sec-websocket-accept'] !== expected || response.headers['sec-websocket-protocol'] !== PROTOCOL) { socket.destroy(); reject(new Error('gateway WS handshake identity is invalid')); return; }
131
+ const value = new GatewayDesktopClient(socket, { openedAt });
132
+ if (head.length) value.parse(head);
133
+ resolve(value);
134
+ });
135
+ request.on('response', response => { response.resume(); reject(Object.assign(new Error(`gateway WS upgrade refused: HTTP ${response.statusCode}`), { status: response.statusCode })); });
136
+ request.on('timeout', () => request.destroy(new Error('gateway WS upgrade timeout')));
137
+ request.on('error', reject); request.end();
138
+ });
139
+ client.ticketMs = openedAt - began; client.handshakeMs = Date.now() - openedAt;
140
+ const ready = await client.request({ type: 'open', botId, taskId, mode: mode === 'read' ? 'read' : 'observe' }, value => value.type === 'ready');
141
+ return { client, ready };
142
+ }