@noob-stupid/dsh-plugin-console 0.3.66 → 0.4.0

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 (50) hide show
  1. package/README.md +1 -1
  2. package/README.zh.md +1 -1
  3. package/lib/client.js +127 -32
  4. package/lib/index.js +60 -9687
  5. package/lib/server/domain/ai-run.js +479 -0
  6. package/lib/server/domain/ai.js +246 -0
  7. package/lib/server/domain/compat.js +474 -0
  8. package/lib/server/domain/components.js +108 -0
  9. package/lib/server/domain/dep-source.js +122 -0
  10. package/lib/server/domain/format-contract.js +265 -0
  11. package/lib/server/domain/format-scan.js +431 -0
  12. package/lib/server/domain/framework.js +393 -0
  13. package/lib/server/domain/install-job.js +561 -0
  14. package/lib/server/domain/install.js +599 -0
  15. package/lib/server/domain/jobs.js +28 -0
  16. package/lib/server/domain/market.js +409 -0
  17. package/lib/server/domain/patch.js +203 -0
  18. package/lib/server/domain/presets.js +93 -0
  19. package/lib/server/domain/quarantine.js +224 -0
  20. package/lib/server/domain/release-source.js +504 -0
  21. package/lib/server/domain/repoland.js +119 -0
  22. package/lib/server/domain/revoke.js +184 -0
  23. package/lib/server/domain/runtime.js +118 -0
  24. package/lib/server/domain/selfupdate.js +319 -0
  25. package/lib/server/domain/skills.js +234 -0
  26. package/lib/server/domain/sources.js +297 -0
  27. package/lib/server/domain/suite.js +220 -0
  28. package/lib/server/infra/exec.js +98 -0
  29. package/lib/server/infra/fsx.js +163 -0
  30. package/lib/server/infra/fw-integrity-check.js +37 -0
  31. package/lib/server/infra/http.js +373 -0
  32. package/lib/server/infra/httpd.js +51 -0
  33. package/lib/server/infra/mask.js +19 -0
  34. package/lib/server/infra/paths.js +177 -0
  35. package/lib/server/infra/semver.js +168 -0
  36. package/lib/server/routes/ai.js +172 -0
  37. package/lib/server/routes/components.js +254 -0
  38. package/lib/server/routes/framework-preflight.js +154 -0
  39. package/lib/server/routes/framework-upgrade.js +679 -0
  40. package/lib/server/routes/framework.js +544 -0
  41. package/lib/server/routes/github-login.js +198 -0
  42. package/lib/server/routes/index.js +128 -0
  43. package/lib/server/routes/install.js +116 -0
  44. package/lib/server/routes/market.js +415 -0
  45. package/lib/server/routes/plugins.js +562 -0
  46. package/lib/server/routes/skills.js +107 -0
  47. package/lib/server/routes/sources.js +437 -0
  48. package/lib/server/routes/state.js +125 -0
  49. package/lib/server/state.js +22 -0
  50. package/package.json +1 -1
@@ -0,0 +1,246 @@
1
+ // L1 · domain —— ai.js(AI 赋能的纯逻辑部分:任务表与视图、DeepSeek 密钥与配置、占位符/Python/VLM 解析、写入白名单与运行白名单、内置计划模板、计划 JSON 解析;分层 Step 7 从 lib/index.js 搬出,只搬移未改逻辑。注:aiEmpowerPlan/Execute、runAiStep、aiRepair 吃运行上下文,留到 Step 8)
2
+ // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
3
+
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
5
+ import { dirname, join, basename, resolve } from 'node:path'
6
+ import { homedir } from 'node:os'
7
+ import { maskUrl } from '../infra/mask.js'
8
+ import { aiJobsFile } from '../infra/paths.js'
9
+
10
+ /** AI 赋能任务注册表(规划/执行均常驻服务端,面板轮询进度)。 */
11
+ const aiJobs = new Map()
12
+
13
+ function saveAiJobs() {
14
+ try {
15
+ mkdirSync(dirname(aiJobsFile()), { recursive: true })
16
+ const arr = [...aiJobs.values()].map((j) => ({ id: j.id, source: j.source, status: j.status, stage: j.stage, plan: j.plan ?? null, logText: j.logText ?? '', stepStates: j.stepStates ?? [], progress: j.progress ?? null, error: j.error ?? null, createdAt: j.createdAt, finishedAt: j.finishedAt ?? null }))
17
+ writeFileSync(aiJobsFile(), JSON.stringify(arr, null, 2), 'utf8')
18
+ } catch {}
19
+ }
20
+
21
+ function loadAiJobs() {
22
+ try {
23
+ if (!existsSync(aiJobsFile())) return
24
+ const arr = JSON.parse(readFileSync(aiJobsFile(), 'utf8'))
25
+ for (const j of arr) {
26
+ if (!j || typeof j.id !== 'string') continue
27
+ let job = j
28
+ if (job.status === 'running') {
29
+ job = { ...job, status: 'failed', error: 'DSH 重启,任务中断(可重新发起)', finishedAt: Date.now() }
30
+ }
31
+ aiJobs.set(job.id, job)
32
+ }
33
+ } catch {}
34
+ }
35
+
36
+ /** AI 赋能计划/执行任务视图(content 一律剥离,防密钥经轮询泄露)。 */
37
+ function aiJobView(job) {
38
+ return {
39
+ jobId: job.id,
40
+ source: job.source,
41
+ status: job.status,
42
+ stage: job.stage,
43
+ error: job.error ?? null,
44
+ type: job.plan?.type ?? null,
45
+ displayName: job.plan?.displayName ?? null,
46
+ summary: job.plan?.summary ?? null,
47
+ servers: (job.plan?.servers ?? []).map((s) => ({ name: s.name, healthUrl: s.healthUrl ?? null, port: s.port ?? null })),
48
+ steps: (job.plan?.steps ?? []).map((s, i) => ({
49
+ index: i,
50
+ action: s.action,
51
+ description: s.description ?? '',
52
+ path: typeof s.path === 'string' ? resolvePlaceholders(s.path, job) : null,
53
+ package: s.package ?? null,
54
+ url: typeof s.url === 'string' ? maskUrl(s.url) : null,
55
+ name: s.name ?? null,
56
+ })),
57
+ stepStates: job.stepStates ?? [],
58
+ progress: job.progress ?? { done: 0, total: (job.plan?.steps ?? []).length },
59
+ logText: (job.logText ?? '').slice(-6000),
60
+ createdAt: job.createdAt,
61
+ finishedAt: job.finishedAt ?? null,
62
+ workspace: job.parentCwd ?? null,
63
+ frameworkCheck: job.frameworkCheck ?? null,
64
+ }
65
+ }
66
+
67
+ /** 占位符:${home} 家目录 / ${profile} DSH profile 目录 / ${python} Python 解释器 / ${scripts} Python Scripts / ${node} node 可执行 / ${deepseekKey} DSH 凭据中的 DeepSeek 密钥。 */
68
+ let deepseekKeyCache = null
69
+
70
+ function readDeepSeekKey() {
71
+ if (deepseekKeyCache !== null) return deepseekKeyCache
72
+ try {
73
+ const credFile = join(homedir(), '.dsh', '.credentials.yaml')
74
+ if (existsSync(credFile)) {
75
+ const m = /^\s*DEEPSEEK_API_KEY\s*:\s*(\S+)\s*$/mu.exec(readFileSync(credFile, 'utf8'))
76
+ if (m) {
77
+ deepseekKeyCache = m[1]
78
+ return deepseekKeyCache
79
+ }
80
+ }
81
+ } catch {}
82
+ deepseekKeyCache = ''
83
+ return deepseekKeyCache
84
+ }
85
+
86
+ /**
87
+ * AI 赋能模型配置(OpenViking VLM 用):
88
+ * 1) 优先 ~/.dsh/plugin-console/ai-empower.json(独立区块,显式覆盖)
89
+ * 格式:{ "vlm": { "provider": "...", "api_base": "...", "model": "..." }, "api_key": "sk-..." }
90
+ * api_key 缺省时继承 DSH 凭据;vlm 缺省时整体回退 DSH 当前设置。
91
+ * 2) 未配置区块 → 跟随 DSH:settings.yaml 的 agent-default-model + .credentials.yaml 的 DEEPSEEK_API_KEY。
92
+ */
93
+ function readAiEmpowerConfig() {
94
+ try {
95
+ const cfgFile = join(homedir(), '.dsh', 'plugin-console', 'ai-empower.json')
96
+ if (existsSync(cfgFile)) return JSON.parse(readFileSync(cfgFile, 'utf8'))
97
+ } catch {}
98
+ return null
99
+ }
100
+
101
+ function resolveVlmForOpenViking() {
102
+ const custom = readAiEmpowerConfig()
103
+ let base = null
104
+ try {
105
+ const settings = readFileSync(join(homedir(), '.dsh', 'settings.yaml'), 'utf8')
106
+ const m = /agent-default-model:\s*[\s\S]*?provider:\s*(\S+)\s*\n\s*model:\s*(\S+)/u.exec(settings)
107
+ const p = m?.[1] ?? ''
108
+ if (p === 'deepseek-official' || p === 'deepseek') {
109
+ base = { provider: 'openai', api_base: 'https://api.deepseek.com', model: m[2] ?? 'deepseek-v4-flash-vision-exp' }
110
+ }
111
+ } catch {}
112
+ if (base === null) base = { provider: 'openai', api_base: 'https://api.deepseek.com', model: 'deepseek-v4-flash-vision-exp' }
113
+ const vlm = custom?.vlm ?? base
114
+ const apiKey = typeof custom?.api_key === 'string' && custom.api_key !== '' ? custom.api_key : readDeepSeekKey()
115
+ return { provider: vlm.provider ?? 'openai', api_base: vlm.api_base ?? 'https://api.deepseek.com', model: vlm.model ?? 'deepseek-v4-flash-vision-exp', api_key: apiKey }
116
+ }
117
+
118
+ function resolvePlaceholders(value, jobOrEnv) {
119
+ const env = jobOrEnv ?? {}
120
+ const profile = env.profileDir ?? ''
121
+ const python = resolvePythonPath()
122
+ const home = homedir()
123
+ return String(value)
124
+ .replaceAll('${home}', home)
125
+ .replaceAll('${profile}', profile)
126
+ .replaceAll('${python}', python)
127
+ .replaceAll('${scripts}', join(dirname(python), 'Scripts'))
128
+ .replaceAll('${node}', process.execPath)
129
+ .replaceAll('${deepseekKey}', readDeepSeekKey())
130
+ }
131
+
132
+ function resolvePythonPath() {
133
+ const candidates = [join('E:', 'python314', 'python.exe'), join(homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python314', 'python.exe'), 'python.exe']
134
+ for (const c of candidates) {
135
+ if (existsSync(c)) return c
136
+ }
137
+ return 'python.exe'
138
+ }
139
+
140
+ /** 写入路径白名单:只允许 DSH profile、~/.dsh、~/.openviking、~/.cache/openviking、ASCII 数据根。 */
141
+ function isAllowedWritePath(p, profileDir) {
142
+ const norm = (s) => s.replace(/[\\/]+/gu, '/').replace(/\/+$/u, '')
143
+ const target = norm(resolvePlaceholders(p, { profileDir }))
144
+ const roots = [
145
+ norm(homedir()) + '/.dsh',
146
+ norm(homedir()) + '/.openviking',
147
+ norm(homedir()) + '/.cache/openviking',
148
+ norm(homedir()) + '/.local/share/openviking',
149
+ norm(homedir()) + '/.dsh/repos',
150
+ norm(profileDir),
151
+ 'D:/OpenVikingData',
152
+ ]
153
+ return roots.some((root) => target === root || target.startsWith(root + '/'))
154
+ }
155
+
156
+ /** run-cmd 可执行文件白名单(按 basename)。 */
157
+ const ALLOWED_RUN_FILES = new Set(['curl.exe', 'curl', 'git.exe', 'git', 'node.exe', 'node', 'python.exe', 'python', 'py.exe', 'py', 'gh.exe', 'gh', 'npm.cmd', 'npm', 'ov.cmd', 'ov'])
158
+
159
+ const DESTRUCTIVE_RE = /(^|\s)(rm\s+(-[a-zA-Z]*r)|rmdir\s+\/s|del\s+\/s|format\s+[a-zA-Z]:|Remove-Item\b|taskkill\s+\/im|reg\s+delete|schtasks\s+\/delete|shutdown|cipher\s+\/w)/iu
160
+
161
+ function isSafeRunCmd(file, args) {
162
+ const base = basename(String(file)).toLowerCase()
163
+ if (!ALLOWED_RUN_FILES.has(base)) return false
164
+ const joined = [file, ...(args ?? [])].join(' ')
165
+ return !DESTRUCTIVE_RE.test(joined) && !/[&|;`$<>]/u.test(joined)
166
+ }
167
+
168
+ /** 已知组件内置模板:跳过 AI 调研,直接产出经过验证的部署计划(当前仅 OpenViking)。 */
169
+ function builtinPlanFor(source) {
170
+ // 精确匹配 OpenViking 服务器本体(npm/pip 包名 openviking 或 volcengine/OpenViking 仓库),
171
+ // 防止宽泛子串匹配误命中 openclaw_openviking_skill 之类的第三方包(曾实际发生误判)。
172
+ const norm = String(source).trim().toLowerCase()
173
+ .replace(/^https?:\/\//u, '')
174
+ .replace(/^www\./u, '')
175
+ .replace(/^github\.com\//u, '')
176
+ .replace(/^raw\.githubusercontent\.com\//u, '')
177
+ .replace(/\.git$/u, '')
178
+ .replace(/\/$/u, '')
179
+ if (norm !== 'openviking' && norm !== 'volcengine/openviking') return null
180
+ const home = homedir()
181
+ const storage = /^[\x00-\x7F]+$/u.test(home) ? `${home}/.openviking/data` : 'D:/OpenVikingData'
182
+ const vlm = resolveVlmForOpenViking()
183
+ // 注意:路径必须在此处(JSON.stringify 之前)使用真实值,由 stringify 统一转义;
184
+ // 不能在 content 中保留 ${home} 占位符——写文件时替换会把反斜杠路径变成非法 JSON 转义。
185
+ const ovConf = {
186
+ server: { host: '127.0.0.1', port: 1933, cors_origins: ['*'] },
187
+ vlm: { provider: vlm.provider, api_base: vlm.api_base, api_key: vlm.api_key, model: vlm.model },
188
+ embedding: { dense: { provider: 'local', model: 'bge-small-zh-v1.5-f16', model_path: join(home, '.cache', 'openviking', 'models', 'bge-small-zh-v1.5-f16.gguf') } },
189
+ storage: { workspace: storage },
190
+ }
191
+ const server = { name: 'openviking-server', file: '${python}', args: ['-m', 'openviking_cli.server_bootstrap'], cwd: '${home}/.openviking', healthUrl: 'http://127.0.0.1:1933/health', uiUrl: 'http://127.0.0.1:1933/studio', port: 1933 }
192
+ return {
193
+ type: 'service',
194
+ displayName: 'OpenViking(本地记忆服务器)',
195
+ summary: '安装 OpenViking 服务器(Python 3.14 + llama-cpp-python)、下载中文嵌入模型、写入 ov.conf(复用 DSH 的 DeepSeek 凭据、ASCII 存储路径)、启动并健康检查;完成后重启 DSH 会话即可出现 mcp__openviking__* 工具,pending 积压自动回放。',
196
+ servers: [server],
197
+ steps: [
198
+ { action: 'install-pip', package: 'openviking[local-embed]', description: '安装 OpenViking 服务器(含本地嵌入 llama-cpp-python)' },
199
+ { action: 'download', url: 'https://huggingface.co/CompendiumLabs/bge-small-zh-v1.5-gguf/resolve/main/bge-small-zh-v1.5-f16.gguf?download=true', path: '${home}/.cache/openviking/models/bge-small-zh-v1.5-f16.gguf', description: '下载中文本地嵌入模型 bge-small-zh-v1.5-f16(47MB,走 hf-mirror)' },
200
+ { action: 'write-file', path: '${home}/.openviking/ov.conf', content: JSON.stringify(ovConf, null, 2), description: '写入 ov.conf(DeepSeek VLM + 本地嵌入 + dev 免鉴权 + ASCII 存储路径)' },
201
+ { action: 'start-service', ...server, description: '启动 openviking-server(127.0.0.1:1933)' },
202
+ { action: 'wait-health', url: 'http://127.0.0.1:1933/health', timeoutMs: 90000, description: '等待健康检查通过' },
203
+ ],
204
+ }
205
+ }
206
+
207
+ /** AI 赋能预案:把已知环境事实与踩坑点固化为提示,避免子代理每次重新踩坑。 */
208
+ function aiEmpowerPresetFor(source) {
209
+ const common = [
210
+ '## 本机环境事实(务必遵守)',
211
+ '- 网络:pypi.org 不通、清华镜像 403;pip 必须用 `--index-url https://mirrors.aliyun.com/pypi/simple/`。npm registry.npmjs.org 可能黑洞,pnpm 用 `--registry https://registry.npmmirror.com`。huggingface.co 被墙,模型文件一律用 `https://hf-mirror.com/<同一路径>`(执行器会自动把 huggingface.co 换成 hf-mirror.com,计划里也可直接写 hf-mirror)。',
212
+ '- 用户名含中文(花火):任何写盘路径若含中文,各语言的 Rust/原生向量库会报 UnicodeDecodeError——服务数据目录必须纯 ASCII(如 `D:/OpenVikingData`)。',
213
+ '- 凭据复用:DSH 的 DeepSeek 密钥在 `${home}/.dsh/.credentials.yaml`(YAML 行 `DEEPSEEK_API_KEY: sk-...`);模型选择在 `${home}/.dsh/settings.yaml` 的 `agent-default-model`(provider deepseek-official)。对外 OpenAI 兼容接口:api_base `https://api.deepseek.com`,模型名 `deepseek-v4-flash-vision-exp`。',
214
+ '- 步骤解释用中文;每个动作一个步骤;步骤数尽量少;不得包含任何破坏性命令;服务类组件必须配 start-service + wait-health。',
215
+ ]
216
+ if (/openviking/i.test(source)) {
217
+ return [
218
+ ...common,
219
+ '## OpenViking 已验证的部署事实(v0.4.17,Python 3.14)',
220
+ '- 安装:`pip install openviking[local-embed]`(含 llama-cpp-python 0.3.35,cp310-abi3 轮子可用;无需加 --force-reinstall)。',
221
+ '- 嵌入模型:`bge-small-zh-v1.5-f16.gguf`(47MB,512 维中文)下载到 `${home}/.cache/openviking/models/`(写 download 步骤;URL 写 huggingface.co 会被执行器自动换 hf-mirror.com)。',
222
+ '- ov.conf 路径 `${home}/.openviking/ov.conf`,内容:vlm = { provider: openai, api_base: https://api.deepseek.com, model: deepseek-v4-flash-vision-exp, api_key: 从 `${home}/.dsh/.credentials.yaml` 读取并内联 };embedding.dense = { provider: local, model: bge-small-zh-v1.5-f16, model_path: <上面的 gguf 路径> };server = { host: 127.0.0.1, port: 1933 }(不要写 root_api_key,dev 免鉴权模式自动启用);storage.workspace = `${home}` 含非 ASCII 时写 `D:/OpenVikingData`,否则 `${home}/.openviking/data`。',
223
+ '- 启动:start-service,file=`${python}`,args=`["-m","openviking_cli.server_bootstrap"]`,cwd=`${home}/.openviking`,healthUrl=`http://127.0.0.1:1933/health`,port=1933。服务器启动首次会建 `D:/OpenVikingData` 数据目录。',
224
+ '- 部署完成后惯例:重启 DSH 会话后 mcp__openviking__* 工具出现,`${home}/.openviking/pending` 的积压消息会在新会话启动时自动回放——把这写进总结。',
225
+ ].join('\n')
226
+ }
227
+ return common.join('\n')
228
+ }
229
+
230
+ /** 从子代理输出中提取 ```json ... ``` 计划。 */
231
+ function parsePlanJson(text) {
232
+ const m = /```json\s*([\s\S]*?)```/u.exec(text)
233
+ const raw = m ? m[1] : text
234
+ try {
235
+ const plan = JSON.parse(raw)
236
+ if (!plan || typeof plan !== 'object' || !Array.isArray(plan.steps)) return null
237
+ for (const s of plan.steps) {
238
+ if (typeof s.action !== 'string' || !['install-pip', 'install-npm', 'write-file', 'download', 'run-cmd', 'start-service', 'wait-health', 'register-component'].includes(s.action)) return null
239
+ }
240
+ return plan
241
+ } catch {
242
+ return null
243
+ }
244
+ }
245
+
246
+ export { aiJobs, saveAiJobs, loadAiJobs, aiJobView, deepseekKeyCache, readDeepSeekKey, readAiEmpowerConfig, resolveVlmForOpenViking, resolvePlaceholders, resolvePythonPath, isAllowedWritePath, ALLOWED_RUN_FILES, DESTRUCTIVE_RE, isSafeRunCmd, builtinPlanFor, aiEmpowerPresetFor, parsePlanJson }