@luchenglong/cli-bridge 0.2.9 → 0.2.11

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.
@@ -568,1392 +568,1440 @@ function createRunner(o) {
568
568
  return { coreExecute, runSync, askFallback }
569
569
  }
570
570
 
571
- // providers/codebuddy.mjs — codebuddy provider descriptor。
572
- //
573
- // 移植基线:codebuddy-first-bridge v1.6.x 的纯逻辑与 preset/codebuddy-first 内联语义,
574
- // 逐字保持(契约 §8:工具入参语义与 schema 不许漂移)。provider 无关骨架在 bridge-kit。
575
- // 纯逻辑片:codebuddy-catalog.mjs(模型目录/凭据/探测)、codebuddy-schemas.mjs(三 schema)、
576
- // codebuddy-models-tool.mjs(codebuddy_models 工具)。build 会把 codebuddy-*.mjs 全部
577
- // 内联进沙箱同一作用域。
578
- //
579
- // 红线:本文件会被内联进 dynamic 沙箱 —— 绝不出现元环境字面量;process/env/fs 只经
580
- // deps 注入或 typeof 守卫读取;宿主能力全部走 attach(hostOpts)。isLimited 用本文件
581
- // 自定义的中文限流表(旧 core verbatim),不用 kit 通用表。
582
- // ── 提示词策略段(旧 preset POLICY_TEXT verbatim;systemPrompt order 5)────────
583
- // (LIMIT_RE 在 kit L33 已有,故此处带 CODEBUDDY_ 前缀)。
584
- // codebuddy-en 凭据的第三条通道:DSH 凭据库里可能已有的 workbuddy key(旧 core verbatim)。
585
-
586
- // 离线/读不到描述文件时的回退静态表(真机按账号实测快照,2026-09)。
587
- // 模型目录相关常量与纯逻辑已拆到 ./codebuddy-catalog.mjs —— 主文件原 864 行远超
588
- // 600 行红线(规则 §26)。
589
- // • ESM 形态(preset / MCP / 测试):直接 import。
590
- // • dynamic 沙箱形态:scripts/build.mjs 把该文件内联进同一作用域(模板顶部的
591
- // 第三个占位标记),import 行被 stripModuleSyntax 剥掉,同名标识符天然可见。
592
- // ⚠️ 本注释里绝不写出那个标记的字面量:build 做全局文本替换,注释中出现同样
593
- // 会被替换成正文代码,把注释劈开 → 生成物语法崩。
594
- function defineProvider (deps = {}) {
595
- const env = deps.env || {}
596
- let host = null // attach 后保存 hostOpts(preset/dynamic/MCP 形态宿主能力)
597
-
598
- // 宿主 fs/path 访问的统一入口:deps 注入优先;dynamic 内联体(无 import,运行在
599
- // 后端 Node 上下文)回退到 getBuiltinModule —— 与旧 core builtinModule() 的
600
- // typeof 守卫模式一致(这是旧 dynamic 端点修复生效的同一条通道,缺了它默认
601
- // 后端会回到 401 老病)。两者皆不可用时自然降级:读不到文件 = 静态表兜底。
602
- function builtinModule(name) {
603
- try {
604
- const proc = globalThis.process
605
- return (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule(name) : null
606
- } catch (e) { return null }
607
- }
608
- function fsRead(p) {
609
- const nf = deps.nodeFs || builtinModule('node:fs')
610
- if (!nf || typeof nf.readFileSync !== 'function') return null
611
- try { return nf.readFileSync(p, 'utf8') } catch (e) { return null }
612
- }
613
- function pathJoin(...parts) {
614
- const np = deps.nodePath || builtinModule('node:path')
615
- if (np && typeof np.join === 'function') return np.join(...parts)
616
- return parts.join('/')
617
- }
618
- const pathSep = (deps.nodePath && deps.nodePath.sep) || (/[A-Za-z]:[\\/]/.test(env.USERPROFILE || env.APPDATA || '') ? '\\' : '/')
619
-
620
- // 把宿主真实能力注入 catalog 模块:它从本闭包搬出后拿不到 env / fsRead / pathJoin /
621
- // pathSep。未注入时 catalog 退到 getBuiltinModule 守卫(绝不抛)。内联到 dynamic
622
- // 沙箱时 cbCatalogSetHost 同作用域可见,行为一致。
623
- try {
624
- cbCatalogSetHost(env, deps.nodePath || builtinModule('node:path'), deps.nodeFs || builtinModule('node:fs'))
625
- } catch (e) { }
626
-
627
- // ── 端点/登录域(旧 core endpointHost verbatim:非法 URL 容错)───────────────
628
- function endpointHost(url) {
629
- const s = String(url || '').trim()
630
- if (!s) return ''
631
- try { return new URL(s).hostname.toLowerCase() } catch (e) { }
632
- const m = /^[a-z]+:\/\/([^/?#]+)/i.exec(s)
633
- return m ? m[1].toLowerCase() : ''
634
- }
635
-
636
- function resolveEndpoint(explicitBaseUrl, authDomain) {
637
- const explicit = String(explicitBaseUrl || '').trim()
638
- if (explicit) return explicit
639
- const d = String(authDomain || '').trim().toLowerCase()
640
- if (d && AUTH_DOMAIN_ENDPOINTS[d]) return AUTH_DOMAIN_ENDPOINTS[d]
641
- return null
642
- }
643
-
644
- function isBackend(v) { return BACKENDS.indexOf(v) >= 0 }
645
-
646
- // 后端名规范化(v1.3.3 verbatim):别名 + 大小写不敏感;非后端名原样返回。
647
- function normalizeBackend(v) {
648
- if (typeof v !== 'string') return v
649
- const lower = v.trim().toLowerCase()
650
- const aliased = BACKEND_ALIASES[lower] || lower
651
- return BACKENDS.indexOf(aliased) >= 0 ? aliased : v
652
- }
653
-
654
- // 后端解析优先级(旧 core verbatim):显式 > 会话归属 > 用户偏好 > 默认。
655
- function resolveBackend(args, target, preferredBackend) {
656
- if (args && isBackend(normalizeBackend(args.backend))) return normalizeBackend(args.backend)
657
- if (target && target.backend && isBackend(normalizeBackend(target.backend))) return normalizeBackend(target.backend)
658
- if (preferredBackend && isBackend(normalizeBackend(preferredBackend))) return normalizeBackend(preferredBackend)
659
- return DEFAULT_BACKEND
660
- }
661
-
662
- // 本次调用的端点/凭据 env 注入(旧 core endpointEnv verbatim 三规则)。
663
- function endpointEnv(backend, explicitBaseUrl, authDomain, authToken) {
664
- const out = {}
665
- const endpoint = resolveEndpoint(explicitBaseUrl, authDomain)
666
- const product = BACKEND_ENDPOINTS[backend]
667
- if (endpoint && endpointHost(endpoint) !== endpointHost(product)) {
668
- out.CODEBUDDY_BASE_URL = endpoint
669
- }
670
- if (backend === 'codebuddy-en') {
671
- const tok = String(authToken || '').trim()
672
- if (tok) out.CODEBUDDY_AUTH_TOKEN = tok
673
- }
674
- return Object.keys(out).length ? out : null
675
- }
676
-
677
- // 可操作提示(旧 core endpointMismatchHint 三分支 verbatim)。
678
- function endpointMismatchHint(backend, authDomain, hasToken) {
679
- if (backend === 'codebuddy-en' && !hasToken) {
680
- return 'codebuddy-en(WorkBuddy 国际版)没有可用凭据。国际版的登录 token 被桌面 App 的 protector key 封装,该密钥不落盘、headless CLI 无法自行读取(CLI 自身报 category:"missing-key")。桥接会依次尝试:插件设置 codebuddyEnToken → 环境变量 CODEBUDDY_AUTH_TOKEN → DSH 凭据库(.credentials.yaml / .env 里的 WORKBUDDY_TOKEN)——三者皆空。请在 DSH 里配好 workbuddy provider 的 key,或把国际版 token 填入插件设置;也可直接用 backend="workbuddy"(国内版桌面 CLI,免配置)。'
681
- }
682
- if (backend === 'codebuddy-intl') {
683
- return 'codebuddy-intl(CodeBuddy 国际版)使用同一个 npm CLI,但国际面模型由服务端按账号授权放行:未授权时 CLI 返回 400「model [...] is only available for authorized users」。若你用的是国内账号,请改用 backend="codebuddy"(国内面模型清单)或 backend="codebuddy-en"(WorkBuddy 国际版)。'
684
- }
685
- const d = String(authDomain || '').trim().toLowerCase()
686
- if (d && endpointHost(resolveEndpoint(null, d)) !== endpointHost(BACKEND_ENDPOINTS[backend])) {
687
- return backend + ' 的登录域是 ' + d + ',与产品端点 ' + BACKEND_ENDPOINTS[backend] + ' 不一致;桥接层已自动注入 CODEBUDDY_BASE_URL=' + AUTH_DOMAIN_ENDPOINTS[d] + '。若仍 401,请重新登录对应客户端。'
688
- }
689
- return null
690
- }
691
-
692
- // 从 auth 库读某后端登录域(旧 core readBackendAuthDomain verbatim 语义;fs 经
693
- // deps 注入。io 可覆盖 readFileSync/dir/env,供测试夹具与 MCP 面注入真实 fs)。
694
-
695
- // ── 模型目录(面板/诊断用;无促销角标版,见头注 TODO)───────────────────────
696
- // ── 设置清洗(旧 normalizeBridgeSettings verbatim;updatedAt 透传)───────────
697
- function normalizeSettings(raw) {
698
- const snap = { preferredBackend: DEFAULT_BACKEND, defaultModel: '', codebuddyEnToken: '', endpointOverride: '' }
699
- if (!raw || typeof raw !== 'object') return snap
700
- if (typeof raw.preferredBackend === 'string') {
701
- const nb = normalizeBackend(raw.preferredBackend)
702
- if (typeof nb === 'string' && BACKENDS.indexOf(nb) >= 0) snap.preferredBackend = nb
703
- }
704
- if (typeof raw.defaultModel === 'string') snap.defaultModel = raw.defaultModel.trim()
705
- if (typeof raw.codebuddyEnToken === 'string') snap.codebuddyEnToken = raw.codebuddyEnToken.trim()
706
- if (typeof raw.endpointOverride === 'string') snap.endpointOverride = raw.endpointOverride.trim()
707
- if (typeof raw.updatedAt === 'number' && raw.updatedAt > 0) snap.updatedAt = raw.updatedAt
708
- return snap
709
- }
710
-
711
- // 诊断(旧 diagnoseBackend verbatim;fs/env 走 deps)。
712
- function diagnoseBackend(backend, settings, io) {
713
- const o = io || {}
714
- const e = o.env || env
715
- const s = settings || normalizeSettings(null)
716
- const authDomain = readBackendAuthDomain(backend, Object.assign({ env: e }, o))
717
- const override = String(s.endpointOverride || '').trim()
718
- const byDomain = resolveEndpoint(null, authDomain)
719
- const endpoint = override || byDomain || BACKEND_ENDPOINTS[backend] || null
720
- const endpointSource = override ? 'override' : (byDomain ? 'auth-domain' : 'product')
721
- const tok = resolveEnToken(backend === 'codebuddy-en' ? s.codebuddyEnToken : null, { env })
722
- return {
723
- backend: backend,
724
- authDomain: authDomain,
725
- endpoint: endpoint,
726
- endpointSource: endpointSource,
727
- tokenSource: backend === 'codebuddy-en' ? tok.source : 'not-needed',
728
- tokenHint: tok.source === 'setting' ? '来自设置面板'
729
- : tok.source === 'env' ? '来自环境变量 CODEBUDDY_AUTH_TOKEN'
730
- : tok.source === 'dsh-store' ? '来自 DSH 凭据库(自动复用 WORKBUDDY_TOKEN)'
731
- : backend === 'codebuddy-en' ? '缺失:国际版 token 不落盘,需在设置面板填写或先在 DSH 配 workbuddy provider'
732
- : '无需填写(国内版由 CLI 自行登录)',
733
- mismatch: endpointMismatchHint(backend, authDomain, !!tok.token) !== null,
734
- hint: endpointMismatchHint(backend, authDomain, !!tok.token) || null,
735
- defaultModel: String(s.defaultModel || '').trim() || '(CLI 默认)'
736
- }
737
- }
738
-
739
- // ── 结果判定(旧 isLimited/isTransientCliError/failureHint verbatim)─────────
740
- function isLimitedCodebuddy(res) {
741
- if (!res || res.ok) return false
742
- // 前置凭据缺失:配置问题,不是限流(弹窗重试不会让 token 出现)。
743
- if (res.status === 'AUTH_REQUIRED') return false
744
- if (res.status === 'SPAWN_ERROR' || res.status === 'CODEBUDDY_UNAVAILABLE' || res.status === 'HUNG_TIMEOUT') return true
745
- const hay = String(res.stderr || '') + ' ' + String(res.status || '')
746
- // 401/端点域不匹配:需要用户换登录域,不归限流类(旧 v1.3.0 verbatim)。
747
- if (/401 authentication failed|differs from the current product endpoint/i.test(hay)) return false
748
- return CODEBUDDY_LIMIT_RE.test(hay)
749
- }
750
-
751
- function isTransientCliError(res) {
752
- if (!res || res.ok) return false
753
- if (isLimitedCodebuddy(res)) return false
754
- return res.status === 'ERROR_DURING_EXECUTION'
755
- }
756
-
757
- function failureHint(res) {
758
- if (!res || res.ok) return ''
759
- // AUTH_REQUIRED:stderr 已带完整指引,透传(旧 v1.3.1 verbatim)。
760
- if (res.status === 'AUTH_REQUIRED') return '[诊断] ' + String(res.stderr || '')
761
- if (isTransientCliError(res)) {
762
- return res.retried
763
- ? '[诊断] codebuddy CLI 侧瞬时错误(error_during_execution,CLI 未给出原因),已自动重试 1 次仍失败。可换 model 再试一次;若仍失败请改用原生工具完成,或告知用户。'
764
- : '[诊断] codebuddy CLI 侧瞬时错误(error_during_execution,CLI 未给出原因)。直接重试同一请求通常即可通过;也可显式指定 model。不要据此认为任务本身有问题。'
765
- }
766
- if (res.status === 'PARSE_ERROR') {
767
- return '[诊断] 未能从 codebuddy 输出解析出 result 事件(进程可能被中断或输出被截断)。可重试;持续出现请检查 codebuddy CLI 版本。'
768
- }
769
- if (/401 authentication failed|differs from the current product endpoint/i.test(String(res.stderr || ''))) {
770
- return '[诊断] 认证失败(401):请求被送到的端点与当前 CLI 登录域不匹配(国际/国内登录互斥)。请让用户在对应产品域完成 `codebuddy` CLI 登录后重试;这不是限流,重试与回退都无效。'
771
- }
772
- return ''
773
- }
774
-
775
- // stream-json 解析(旧 parseCodebuddyJson verbatim)。
776
- function parseCodebuddyJson(stdoutText) {
777
- const trimmed = String(stdoutText || '').trim()
778
- if (!trimmed) return null
779
- const lines = trimmed.split(/\r?\n/).filter(Boolean)
780
- for (let i = lines.length - 1; i >= 0; i--) {
781
- const ln = lines[i].trim()
782
- if (!ln.startsWith('{')) continue
783
- try {
784
- const obj = JSON.parse(ln)
785
- if (obj && obj.type === 'result') return obj
786
- } catch (e) {}
787
- }
788
- try { return JSON.parse(trimmed) } catch (e) {}
789
- return null
790
- }
791
-
792
- // result 事件 → 统一结果对象(旧 buildResult verbatim,含账号清单收割)。
793
- function buildResult(parsed, outcome, mode, stderrText, stdoutText, backend) {
794
- const bk = backend || 'codebuddy'
795
- const exitCode = outcome ? outcome.exitCode : null
796
- const errText = parsed && typeof parsed.error === 'string' && parsed.error ? parsed.error : ''
797
- const stderr = (stderrText ? String(stderrText).slice(-2000) : '') + (errText ? (stderrText ? ' ' : '') + errText : '')
798
- // v1.6.0:顺手收割服务端回吐的「本账号可用模型」清单(失败静默)。
799
- try {
800
- const harvest = parseAccountModels(stderr) || parseAccountModels(String(stdoutText || ''))
801
- if (harvest && harvest.length) rememberAccountCatalog(bk, harvest)
802
- } catch (e) { /* 收割失败不影响主流程 */ }
803
- if (parsed && parsed.type === 'result') {
804
- const isOk = exitCode === 0 && parsed.is_error === false && parsed.subtype === 'success'
805
- let modelUsed = null
806
- try {
807
- if (parsed.modelUsage && typeof parsed.modelUsage === 'object') {
808
- const names = Object.keys(parsed.modelUsage)
809
- if (names.length) modelUsed = names.join('+')
810
- }
811
- } catch (e) { }
812
- return {
813
- ok: isOk,
814
- status: String(parsed.subtype || (isOk ? 'SUCCESS' : 'ERROR')).toUpperCase(),
815
- response: typeof parsed.result === 'string' ? parsed.result : '',
816
- sessionId: typeof parsed.session_id === 'string' ? parsed.session_id : null,
817
- durationSeconds: typeof parsed.duration_ms === 'number' ? parsed.duration_ms / 1000 : null,
818
- numTurns: typeof parsed.num_turns === 'number' ? parsed.num_turns : null,
819
- totalTokens: (parsed.usage && typeof parsed.usage.input_tokens === 'number' && typeof parsed.usage.output_tokens === 'number')
820
- ? (parsed.usage.input_tokens + parsed.usage.output_tokens) : null,
821
- exitCode: exitCode,
822
- mode: mode,
823
- backend: bk,
824
- modelUsed: modelUsed,
825
- stderr: stderr
826
- }
827
- }
828
- return {
829
- ok: false,
830
- status: 'PARSE_ERROR',
831
- response: '',
832
- sessionId: null,
833
- durationSeconds: null,
834
- numTurns: null,
835
- totalTokens: null,
836
- exitCode: exitCode,
837
- mode: mode,
838
- backend: bk,
839
- stderr: stderr,
840
- rawStdout: String(stdoutText || '').slice(-2000)
841
- }
842
- }
843
-
844
- // 统一 argv 构造(旧 buildArgv verbatim;env 端点/凭据注入在本 provider 内完成,
845
- // 因为 kit 只透传 opts,宿主三形态都从本函数的返回值取 built.env 下发 spawn)。
846
- function buildArgvCodebuddy(prefix, args, opts) {
847
- const o = opts || {}
848
- const argv = [prefix[0], ...prefix.slice(1), '-p', String(args.prompt), '--output-format', 'stream-json']
849
- let mode = args.mode || o.defaultMode || 'auto'
850
- if (mode === 'auto') mode = o.planActive ? 'plan' : 'bypassPermissions'
851
- if (mode === 'plan') {
852
- argv.push('--permission-mode', 'plan')
853
- argv.push('--allowedTools', 'Bash')
854
- } else {
855
- mode = 'bypassPermissions'
856
- argv.push('--permission-mode', 'bypassPermissions')
857
- }
858
- // 模型优先级:显式 args.model > 用户偏好 defaultModel > CLI 默认。
859
- const model = args.model || o.defaultModel
860
- if (model) argv.push('--model', String(model))
861
- if (args.effort) argv.push('--effort', String(args.effort))
862
- if (args.maxTurns !== undefined && args.maxTurns !== null) argv.push('--max-turns', String(clampInt(args.maxTurns, 50, 1, 500)))
863
- if (Array.isArray(args.addDirs)) for (const d of args.addDirs) { if (d) argv.push('--add-dir', String(d)) }
864
- if (args.sessionId) argv.push('--resume', String(args.sessionId))
865
- else if (args.continueLatest) argv.push('--continue')
866
- const timeoutSec = clampInt(args.timeoutSec, 300, 10, 3600)
867
- // 端点/凭据注入(旧 preset/dynamic/MCP 三形态一致语义,宿主不必自算):
868
- // authDomain 只在有 fs 能力的宿主读得到;dynamic 沙箱 → 沿用 CLI 自身端点。
869
- const bk = o.backend || DEFAULT_BACKEND
870
- const settings = o.settings || null
871
- const authDomain = readBackendAuthDomain(bk, { env })
872
- const tok = resolveEnToken(bk === 'codebuddy-en' && settings ? settings.codebuddyEnToken : null, { env })
873
- const inject = endpointEnv(bk, settings ? settings.endpointOverride : null, authDomain, tok.token)
874
- return { argv: argv, timeoutSec: timeoutSec, mode: mode, env: o.env || inject }
875
- }
876
-
877
- // 流事件折叠(旧 foldEvent 主体 verbatim;project 状态由 kit engine 持有)。
878
- function foldStreamEvent(ev, p, summarizeArgs, MAX_ARG_LEN, MAX_TRAIL) {
879
- if (!ev || !ev.type) return
880
- if (ev.type === 'assistant' && ev.message && Array.isArray(ev.message.content)) {
881
- for (const block of ev.message.content) {
882
- if (block.type === 'tool_use') {
883
- p._stepCounter = (p._stepCounter || 0) + 1
884
- const stepIndex = p._stepCounter
885
- if (!p._toolMap) p._toolMap = {}
886
- p._toolMap[block.id] = { stepIndex: stepIndex, name: block.name }
887
- const args = summarizeArgs(block.input)
888
- p.trail.push({ stepIndex: stepIndex, state: 'ACTIVE', tool: block.name, args: args, at: new Date().toISOString() })
889
- if (p.trail.length > MAX_TRAIL) p.trail.shift()
890
- p.current = { tool: block.name, args: args, stepIndex: stepIndex, since: new Date().toISOString() }
891
- } else if (block.type === 'thinking') {
892
- p.current = { tool: 'thinking', args: { text: String(block.thinking || '').slice(0, MAX_ARG_LEN) }, stepIndex: 0, since: new Date().toISOString() }
893
- } else if (block.type === 'text') {
894
- p.current = { tool: 'typing', args: { text: String(block.text || '').slice(0, MAX_ARG_LEN) }, stepIndex: 0, since: new Date().toISOString() }
895
- }
896
- }
897
- } else if (ev.type === 'user' && ev.message && Array.isArray(ev.message.content)) {
898
- for (const block of ev.message.content) {
899
- if (block.type === 'tool_result' && p._toolMap && p._toolMap[block.tool_use_id]) {
900
- const rec = p._toolMap[block.tool_use_id]
901
- p.trail.push({ stepIndex: rec.stepIndex, state: block.is_error ? 'ERROR' : 'DONE', tool: rec.name, args: null, at: new Date().toISOString() })
902
- if (p.trail.length > MAX_TRAIL) p.trail.shift()
903
- if (p.current && p.current.stepIndex === rec.stepIndex) p.current = null
904
- }
905
- }
906
- }
907
- }
908
-
909
- const descriptor = {
910
- id: 'codebuddy',
911
- label: 'CB',
912
- presetId: 'codebuddy-first',
913
- defaultBackend: DEFAULT_BACKEND,
914
- defaultMode: 'auto',
915
- exeName: 'codebuddy',
916
- binEnvVars: ['CODEBUDDY_BIN', 'CODEBUDDY_EN_BIN', 'WORKBUDDY_BIN'],
917
- unavailableStatus: 'CODEBUDDY_UNAVAILABLE',
918
- // 旧 renderResult verbatim 语义:标头用具体 backend('workbuddy OK'),仅当结果
919
- // 无 backend 字段时回落 'codebuddy'(= provider.id)。agy/mimo 旧包标头硬编码
920
- // provider 名,不设此旗。
921
- backendLabel: true,
922
-
923
- backends: () => BACKENDS.slice(),
924
- normalizeBackend,
925
- resolveBackend,
926
-
927
- buildArgv: buildArgvCodebuddy,
928
- parseJson: parseCodebuddyJson,
929
- buildResult,
930
- foldStreamEvent,
931
- isLimited: isLimitedCodebuddy,
932
- isTransientCliError,
933
- failureHint,
934
-
935
- // codebuddy-en 无凭据的确定性前置(v1.3.1 语义,经 kit preflight 钩子实现):
936
- // 三通道皆空 → 直接 AUTH_REQUIRED(可操作指引进 stderr),不白跑一次 spawn。
937
- // 其它后端一律放行。dynamic 沙箱无 fs → dsh-store 通道缺席,abort 提示即可。
938
- preflight({ args, backend, engine, o }) {
939
- if (backend !== 'codebuddy-en') return null
940
- const settings = (o && o.getSettings && o.getSettings()) || null
941
- const tok = resolveEnToken(settings ? settings.codebuddyEnToken : null, { env })
942
- if (tok.token) return null
943
- const authDomain = readBackendAuthDomain('codebuddy-en', { env })
944
- const hint = endpointMismatchHint('codebuddy-en', authDomain, false)
945
- return {
946
- abort: {
947
- ok: false,
948
- status: 'AUTH_REQUIRED',
949
- response: '',
950
- sessionId: null,
951
- durationSeconds: null,
952
- numTurns: null,
953
- totalTokens: null,
954
- exitCode: null,
955
- mode: (args && args.mode) || descriptor.defaultMode || 'auto',
956
- backend: 'codebuddy-en',
957
- stderr: hint
958
- }
959
- }
960
- },
961
-
962
- tools: {
963
- run: {
964
- name: 'codebuddy_run',
965
- description: 'Dispatch a coding/build/debug/investigation task to the local codebuddy agent CLI and return its final answer. Prefer this for implementation, edits, refactors, multi-file investigation and debugging in every mode. DSH fully controls codebuddy: it always runs non-interactively with permissions auto-approved (codebuddy never prompts). Use read-only native tools only for quick lookups and for final build/test verification. In mode=auto the DSH plan state decides plan vs bypassPermissions. Set background=true for long tasks and collect the result via job_output.',
966
- inputSchema: RUN_PARAMS
967
- },
968
- continue: {
969
- name: 'codebuddy_continue',
970
- description: 'Continue an existing codebuddy conversation with a follow-up prompt, reusing codebuddy context. Pass sessionId from a prior codebuddy_run result, or set latest=true to continue the most recent codebuddy conversation. Same DSH-controlled, no-prompt execution as codebuddy_run.',
971
- inputSchema: CONTINUE_PARAMS
972
- },
973
- status: {
974
- name: 'codebuddy_status',
975
- description: 'Read a live snapshot of what the local codebuddy agent is currently doing. Returns one section per project (working directory): running count, the current step (tool name + arguments being executed, or agent_response thinking/typing), the recent step trail (tools executed, done/error), the last completed run status + session id, and per-project cumulative usage (runs + total tokens, since codebuddy exposes no quota API). Optional cwd filters to a single project. Call this to check on an in-flight codebuddy_run/codebuddy_continue without waiting for it to finish.',
976
- inputSchema: STATUS_PARAMS
977
- }
978
- },
979
-
980
- // codebuddy_models:实时列出本机 codebuddy 可用模型(账号探测优先,回落 CLI 名册)。
981
- // 只在宿主有 spawn/fs 能力时注册(preset 注入 deps.process / MCP 注入 deps.nodeChild;
982
- // dynamic 沙箱形态省略)。**失败必须 ok:false + 可操作错误**,绝不静默返回空列表或
983
- // 伪造模型(工作手册红线 §7;对照 workbuddy-auth 的 catalog-store 静默降级 —— 不照抄)。
984
- extraTools: (deps.process || deps.nodeChild || deps.nodeFs) ? [codebuddyModelsTool({
985
- env, deps, BACKEND_ENUM, normalizeBackend, resolveBackend, modelCatalogDetailed, getAccountCatalog,
986
- // faceEnv:让账号探测打到**请求的那个产品面**(与 buildArgv 完全同一套端点/凭据
987
- // 注入规则)。缺了它,探测会走 CLI 自身默认登录面 —— 标着 codebuddy-en 实际却
988
- // 问的是 codebuddy 面,得出的清单看着对其实是另一个面的(本轮修的就是这类偏差)。
989
- faceEnv: (backend) => {
990
- try {
991
- const settings = (deps.settings && typeof deps.settings === 'object') ? deps.settings : {}
992
- const authDomain = readBackendAuthDomain(backend, { env })
993
- const tok = resolveEnToken(backend === 'codebuddy-en' ? settings.codebuddyEnToken : null, { env })
994
- return endpointEnv(backend, settings.endpointOverride, authDomain, tok.token) || {}
995
- } catch (e) { return {} }
996
- }
997
- })] : [],
998
-
999
- // 设置分区(契约 §3):keystone 统一文件 {codebuddy:{...}};旧扁平文件的字段
1000
- // 级迁移由 keystone LEGACY_SOURCES 承担(字段名逐字相同)。
1001
- settings: {
1002
- keys: SETTINGS_KEYS,
1003
- secretKeys: ['codebuddyEnToken'],
1004
- defaults: () => normalizeSettings(null),
1005
- normalize: (raw) => normalizeSettings(raw),
1006
- meta() {
1007
- // defaultModel 下拉 = 账号级实时清单(服务端裁决)。走过的弯路留档:
1008
- // ① 随包冻结的 product.ioa.json → 16 项、2 个 id 已改名(错)
1009
- // ② CLI `--help` 的 "Currently supported:" → 18 项,但那是 CLI 构建期烙进去的,
1010
- // 实测同样滞后(含已废弃的 minimax-m3/kimi-k3-1,缺 glm-5.3-flashx 等)
1011
- // ③ 现在:用无效模型探测让服务端回「该账号当前支持」的 19 项 —— 唯一权威源。
1012
- // 面板同步回调故用 spawnSync 探测并缓存 5 分钟;失败则下拉留空 + 明确提示,
1013
- // 绝不展示已知错误的 roster。面跟随 preferredBackend(旧实现硬编码 codebuddy 面)。
1014
- const face = (currentPreferredBackend() && normalizeBackend(currentPreferredBackend())) || DEFAULT_BACKEND
1015
- const faceEnv = (() => {
1016
- try {
1017
- const s = deps.settings || {}
1018
- const authDomain = readBackendAuthDomain(face, { env })
1019
- const tok = resolveEnToken(face === 'codebuddy-en' ? s.codebuddyEnToken : null, { env })
1020
- return endpointEnv(face, s.endpointOverride, authDomain, tok.token) || {}
1021
- } catch (e) { return {} }
1022
- })()
1023
- const live = cachedCliModels(face, () => probeAccountModelsSync(env, { envExtra: faceEnv, timeoutMs: 12000 }))
1024
- let options = []
1025
- let note = ''
1026
- if (live && live.ok && live.ids.length) {
1027
- const detail = modelCatalogDetailed(face, { env, accountModels: live.ids })
1028
- const byId = new Map(detail.map((x) => [x.id, x]))
1029
- options = live.ids.map((id) => {
1030
- const d = byId.get(id)
1031
- return { value: id, label: (d && d.label) || id }
1032
- })
1033
- note = '账号实时清单(' + live.ids.length + ' 项,服务端裁决);当前面:' + face + '。日常任务优先免费(x0.00)。'
1034
- } else {
1035
- note = '未能读取该账号的模型清单(' + String((live && live.error) || 'unknown') +
1036
- ')。请确认该产品面已登录(在 codebuddy CLI 里完成登录),或手动输入模型 id。'
1037
- }
1038
- return {
1039
- titleSuffix: 'CodeBuddy',
1040
- fields: [
1041
- {
1042
- key: 'preferredBackend', label: '默认后端', type: 'select',
1043
- options: BACKENDS.map((b) => ({ value: b, label: BACKEND_LABELS[b] })),
1044
- note: '新会话未显式指定 backend 时用这里选的后端;单次调用可覆盖。codebuddy-en 需要 codebuddyEnToken 或 DSH 已配 workbuddy key。'
1045
- },
1046
- {
1047
- key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true, placeholder: '(CLI 默认)',
1048
- options,
1049
- note
1050
- },
1051
- { key: 'codebuddyEnToken', label: 'WorkBuddy 国际版 Token', type: 'secret', note: '仅 backend="codebuddy-en" 需要。国际版登录 token 被 protector key 封装不落盘,只能手工提供;留空则自动尝试 DSH 凭据库的 WORKBUDDY_TOKEN。' },
1052
- { key: 'endpointOverride', label: '端点覆盖', type: 'text', placeholder: 'https://…/v2', note: '一般留空:端点按登录域自动对齐(www.codebuddy.cn 等)。仅在需要强制指定 API 端点时填写,且必须自带 /v2。' }
1053
- ]
1054
- }
1055
- },
1056
- diagnose(snap) {
1057
- const s = normalizeSettings(snap)
1058
- const lines = []
1059
- const seen = new Set()
1060
- // 默认后端一行;codebuddy-en 是 4 面里唯一需要凭据的,恒附一行状态。
1061
- for (const b of [s.preferredBackend, 'codebuddy-en']) {
1062
- if (seen.has(b)) continue
1063
- seen.add(b)
1064
- const d = diagnoseBackend(b, s, {})
1065
- lines.push(b + ': ' + (d.endpoint || '?') + '(登录域 ' + (d.authDomain || '未读到') + ',' + d.tokenHint + ')')
1066
- if (d.hint) lines.push(' ⚠ ' + d.hint)
1067
- }
1068
- return lines.join('\n')
1069
- }
1070
- },
1071
-
1072
- mcp: { version: '0.2.2' }
1073
- }
1074
-
1075
- // 三形态共用的工具逻辑(签名保持 keystone/队友接线契约不变)。
1076
- descriptor.runToolLogic = async function (args, exec) {
1077
- return this.runner.coreExecute(args, exec)
1078
- }
1079
- // 旧 preset continueTool.execute verbatim 映射(sessionId 优先,否则 latest)。
1080
- descriptor.continueToolLogic = async function (args, exec) {
1081
- const a = args || {}
1082
- const mapped = { prompt: a.prompt, backend: a.backend, mode: a.mode, model: a.model, effort: a.effort, maxTurns: a.maxTurns, cwd: a.cwd, timeoutSec: a.timeoutSec, background: a.background }
1083
- if (a.sessionId) mapped.sessionId = a.sessionId
1084
- else if (a.latest) mapped.continueLatest = true
1085
- return this.runner.coreExecute(mapped, exec)
1086
- }
1087
- // 旧 statusTool.execute verbatim:cwd 过滤 + 单项目聚合覆写(含 runs/totalTokens)。
1088
- descriptor.statusToolLogic = function (args) {
1089
- const a = args || {}
1090
- const snap = this.engine.statusSnapshot()
1091
- if (a.cwd) {
1092
- const key = String(a.cwd)
1093
- snap.projects = snap.projects.filter((p) => p.cwd === key)
1094
- const g = snap.projects[0]
1095
- if (g) { snap.state = g.state; snap.running = g.running; snap.current = g.current; snap.trail = g.trail; snap.lastStatus = g.lastStatus; snap.lastAt = g.lastAt; snap.lastSessionId = g.lastSessionId; snap.lastBackend = g.lastBackend; snap.fallbackActive = g.fallbackActive; snap.runs = g.runs; snap.totalTokens = g.totalTokens; snap.updatedAt = g.updatedAt }
1096
- }
1097
- return snap
1098
- }
1099
-
1100
- // 供 preset/dynamic/MCP/测试形态复用的领域函数(不 import home-plugin,全部经
1101
- // 描述符暴露;MCP 面据 buildArgv/parseJson/buildResult/isLimited/failureHint 组
1102
- // 装真实 spawn 执行,语义与旧 mcp/codebuddy-mcp-server.mjs 一致)。
1103
- Object.assign(descriptor, {
1104
- policyText: POLICY_TEXT, limits: { LIMIT_RE: CODEBUDDY_LIMIT_RE },
1105
- backendLabels: BACKEND_LABELS, backendEndpoints: BACKEND_ENDPOINTS,
1106
- authDomainEndpoints: AUTH_DOMAIN_ENDPOINTS, backendAuthIds: BACKEND_AUTH_IDS,
1107
- staticModelIds: BACKEND_MODEL_IDS, endpointHost, resolveEndpoint, endpointEnv,
1108
- endpointMismatchHint, resolveEnToken, readDshWorkbuddyToken, readBackendAuthDomain,
1109
- diagnoseBackend, parseAccountModels, rememberAccountCatalog, getAccountCatalog,
1110
- normalizeProductConfig, extractModelIds, extractModelCredits, parseCreditValue,
1111
- formatCreditLabel, lookupModelCredits, normalizeSettings
1112
- })
1113
- // env 经 io 绑定:catalog 是模块级纯函数,若共用模块级 env,多 provider 实例会互相
1114
- // 覆盖(测试串扰)。这里每个实例调用时都带上自己的 env。
1115
- const withEnv = (fn) => (backend, io) => fn(backend, Object.assign({ env }, io || {}))
1116
- Object.assign(descriptor, {
1117
- backendModelCatalog: withEnv(backendModelCatalog),
1118
- modelCatalogDetailed: withEnv(modelCatalogDetailed),
1119
- formatModelCatalogText: withEnv(formatModelCatalogText)
1120
- })
1121
-
1122
- descriptor.attach = function (hostOpts) {
1123
- host = hostOpts || {}
1124
- this.engine = createStatusEngine({ provider: this, publish: host.publish })
1125
- const runner = createRunner(Object.assign({}, host, { provider: this, engine: this.engine }))
1126
- this.runner = runner
1127
- return runner
1128
- }
1129
- descriptor.hostOptions = () => host
1130
- return descriptor
1131
- }
571
+ // providers/codebuddy.mjs — codebuddy provider descriptor。
572
+ //
573
+ // 移植基线:codebuddy-first-bridge v1.6.x 的纯逻辑与 preset 内联语义,逐字保持(契约 §8: // 工具入参语义与 schema 不许漂移)。provider 无关骨架在 bridge-kit。纯逻辑片:
574
+ // codebuddy-catalog.mjs(模型目录/凭据)、codebuddy-models-source.mjs(探测+缓存)、
575
+ // codebuddy-models-tool.mjs(codebuddy_models)、codebuddy-consts.mjs(POLICY_TEXT/后端表)、
576
+ // codebuddy-schemas.mjs(三 schema)。
577
+ //
578
+ // 红线:本文件会被内联进 dynamic 沙箱 —— 绝不出现元环境字面量;process/env/fs 只经 deps
579
+ // 注入或 typeof 守卫读取;宿主能力全部走 attach(hostOpts)。isLimited 用本文件自定义的中文
580
+ // 限流表(旧 core verbatim),不用 kit 通用表。命名纪律:顶层名绝不与 bridge-kit 同名
581
+ // (沙箱里同作用域),故限流表带 CODEBUDDY_ 前缀。
582
+ // ⚠️ 各片在沙箱里被内联进**同一作用域**(build 做全局文本替换):注释里绝不写占位标记的
583
+ // 字面量,否则注释会被替换成正文代码而劈开,生成物语法崩。
584
+ // import { createStatusEngine, createRunner, clampInt } from '../core/bridge-kit.mjs'// import { BACKEND_MODEL_IDS, PRODUCT_DESCRIPTOR_ENV, CREDIT_ID_ALIASES, MODEL_DESCRIPTION, BACKEND_ENUM, accountConfigCandidates, productDescriptorCandidates, normalizeProductConfig, extractModelIds, parseCreditValue, formatCreditLabel, extractModelCredits, lookupModelCredits, backendModelCatalog, rememberAccountCatalog, getAccountCatalog, parseAccountModels, modelCatalogDetailed, formatModelCatalogText, cbCatalogSetHost, BACKEND_AUTH_IDS, CREDENTIAL_KEYS, readBackendAuthDomain, readDshWorkbuddyToken, resolveEnToken } from './codebuddy-catalog.mjs'
585
+ // 探测/缓存那一层单独一片(要走子进程,与纯解析内聚度低;见 codebuddy-models-source.mjs 头注)。
586
+ // import { fetchHelpModelsSync, cachedCliModels, currentPreferredBackend, parseHelpModels, probeAccountModelsSync, rememberCliModels, probeAccountModels } from './codebuddy-models-source.mjs'
587
+ // import { codebuddyModelsTool } from './codebuddy-models-tool.mjs'
588
+ // import { POLICY_TEXT, CODEBUDDY_LIMIT_RE, BACKENDS, BACKEND_ENDPOINTS, AUTH_DOMAIN_ENDPOINTS, BACKEND_LABELS, BACKEND_ALIASES, DEFAULT_BACKEND, SETTINGS_KEYS } from './codebuddy-consts.mjs'
589
+ // import { RUN_PARAMS, CONTINUE_PARAMS, STATUS_PARAMS } from './codebuddy-schemas.mjs'
590
+ function defineProvider (deps = {}) {
591
+ const env = deps.env || {}
592
+ let host = null // attach 后保存 hostOpts(preset/dynamic/MCP 形态宿主能力)
593
+
594
+ // 宿主 fs/path 访问的统一入口:deps 注入优先;dynamic 内联体(无 import,运行在
595
+ // 后端 Node 上下文)回退到 getBuiltinModule —— 与旧 core builtinModule() 的
596
+ // typeof 守卫模式一致(这是旧 dynamic 端点修复生效的同一条通道,缺了它默认
597
+ // 后端会回到 401 老病)。两者皆不可用时自然降级:读不到文件 = 静态表兜底。
598
+ function builtinModule(name) {
599
+ try {
600
+ const proc = globalThis.process
601
+ return (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule(name) : null
602
+ } catch (e) { return null }
603
+ }
604
+ function fsRead(p) {
605
+ const nf = deps.nodeFs || builtinModule('node:fs')
606
+ if (!nf || typeof nf.readFileSync !== 'function') return null
607
+ try { return nf.readFileSync(p, 'utf8') } catch (e) { return null }
608
+ }
609
+ function pathJoin(...parts) {
610
+ const np = deps.nodePath || builtinModule('node:path')
611
+ if (np && typeof np.join === 'function') return np.join(...parts)
612
+ return parts.join('/')
613
+ }
614
+ const pathSep = (deps.nodePath && deps.nodePath.sep) || (/[A-Za-z]:[\\/]/.test(env.USERPROFILE || env.APPDATA || '') ? '\\' : '/')
615
+
616
+ // 把宿主真实能力注入 catalog 模块:它从本闭包搬出后拿不到 env / fsRead / pathJoin /
617
+ // pathSep。未注入时 catalog 退到 getBuiltinModule 守卫(绝不抛)。内联到 dynamic
618
+ // 沙箱时 cbCatalogSetHost 同作用域可见,行为一致。
619
+ try {
620
+ cbCatalogSetHost(env, deps.nodePath || builtinModule('node:path'), deps.nodeFs || builtinModule('node:fs'))
621
+ } catch (e) { }
622
+
623
+ // ── 端点/登录域(旧 core endpointHost verbatim:非法 URL 容错)───────────────
624
+ function endpointHost(url) {
625
+ const s = String(url || '').trim()
626
+ if (!s) return ''
627
+ try { return new URL(s).hostname.toLowerCase() } catch (e) { }
628
+ const m = /^[a-z]+:\/\/([^/?#]+)/i.exec(s)
629
+ return m ? m[1].toLowerCase() : ''
630
+ }
631
+
632
+ function resolveEndpoint(explicitBaseUrl, authDomain) {
633
+ const explicit = String(explicitBaseUrl || '').trim()
634
+ if (explicit) return explicit
635
+ const d = String(authDomain || '').trim().toLowerCase()
636
+ if (d && AUTH_DOMAIN_ENDPOINTS[d]) return AUTH_DOMAIN_ENDPOINTS[d]
637
+ return null
638
+ }
639
+
640
+ function isBackend(v) { return BACKENDS.indexOf(v) >= 0 }
641
+
642
+ // 后端名规范化(v1.3.3 verbatim):别名 + 大小写不敏感;非后端名原样返回。
643
+ function normalizeBackend(v) {
644
+ if (typeof v !== 'string') return v
645
+ const lower = v.trim().toLowerCase()
646
+ const aliased = BACKEND_ALIASES[lower] || lower
647
+ return BACKENDS.indexOf(aliased) >= 0 ? aliased : v
648
+ }
649
+
650
+ // 后端解析优先级(旧 core verbatim):显式 > 会话归属 > 用户偏好 > 默认。
651
+ function resolveBackend(args, target, preferredBackend) {
652
+ if (args && isBackend(normalizeBackend(args.backend))) return normalizeBackend(args.backend)
653
+ if (target && target.backend && isBackend(normalizeBackend(target.backend))) return normalizeBackend(target.backend)
654
+ if (preferredBackend && isBackend(normalizeBackend(preferredBackend))) return normalizeBackend(preferredBackend)
655
+ return DEFAULT_BACKEND
656
+ }
657
+
658
+ // 本次调用的端点/凭据 env 注入(旧 core endpointEnv verbatim 三规则)。
659
+ function endpointEnv(backend, explicitBaseUrl, authDomain, authToken) {
660
+ const out = {}
661
+ const endpoint = resolveEndpoint(explicitBaseUrl, authDomain)
662
+ const product = BACKEND_ENDPOINTS[backend]
663
+ if (endpoint && endpointHost(endpoint) !== endpointHost(product)) {
664
+ out.CODEBUDDY_BASE_URL = endpoint
665
+ }
666
+ if (backend === 'codebuddy-en') {
667
+ const tok = String(authToken || '').trim()
668
+ if (tok) out.CODEBUDDY_AUTH_TOKEN = tok
669
+ }
670
+ return Object.keys(out).length ? out : null
671
+ }
672
+
673
+ // 可操作提示(旧 core endpointMismatchHint 三分支 verbatim)。
674
+ function endpointMismatchHint(backend, authDomain, hasToken) {
675
+ if (backend === 'codebuddy-en' && !hasToken) {
676
+ return 'codebuddy-en(WorkBuddy 国际版)没有可用凭据。国际版的登录 token 被桌面 App 的 protector key 封装,该密钥不落盘、headless CLI 无法自行读取(CLI 自身报 category:"missing-key")。桥接会依次尝试:插件设置 codebuddyEnToken → 环境变量 CODEBUDDY_AUTH_TOKEN → DSH 凭据库(.credentials.yaml / .env 里的 WORKBUDDY_TOKEN)——三者皆空。请在 DSH 里配好 workbuddy provider 的 key,或把国际版 token 填入插件设置;也可直接用 backend="workbuddy"(国内版桌面 CLI,免配置)。'
677
+ }
678
+ if (backend === 'codebuddy-intl') {
679
+ return 'codebuddy-intl(CodeBuddy 国际版)使用同一个 npm CLI,但国际面模型由服务端按账号授权放行:未授权时 CLI 返回 400「model [...] is only available for authorized users」。若你用的是国内账号,请改用 backend="codebuddy"(国内面模型清单)或 backend="codebuddy-en"(WorkBuddy 国际版)。'
680
+ }
681
+ const d = String(authDomain || '').trim().toLowerCase()
682
+ if (d && endpointHost(resolveEndpoint(null, d)) !== endpointHost(BACKEND_ENDPOINTS[backend])) {
683
+ return backend + ' 的登录域是 ' + d + ',与产品端点 ' + BACKEND_ENDPOINTS[backend] + ' 不一致;桥接层已自动注入 CODEBUDDY_BASE_URL=' + AUTH_DOMAIN_ENDPOINTS[d] + '。若仍 401,请重新登录对应客户端。'
684
+ }
685
+ return null
686
+ }
687
+
688
+ // 从 auth 库读某后端登录域(旧 core readBackendAuthDomain verbatim 语义;fs 经
689
+ // deps 注入。io 可覆盖 readFileSync/dir/env,供测试夹具与 MCP 面注入真实 fs)。
690
+
691
+ // ── 模型目录(面板/诊断用;无促销角标版,见头注 TODO)───────────────────────
692
+ // ── 设置清洗(旧 normalizeBridgeSettings verbatim;updatedAt 透传)───────────
693
+ function normalizeSettings(raw) {
694
+ const snap = { preferredBackend: DEFAULT_BACKEND, defaultModel: '', codebuddyEnToken: '', endpointOverride: '' }
695
+ if (!raw || typeof raw !== 'object') return snap
696
+ if (typeof raw.preferredBackend === 'string') {
697
+ const nb = normalizeBackend(raw.preferredBackend)
698
+ if (typeof nb === 'string' && BACKENDS.indexOf(nb) >= 0) snap.preferredBackend = nb
699
+ }
700
+ if (typeof raw.defaultModel === 'string') snap.defaultModel = raw.defaultModel.trim()
701
+ if (typeof raw.codebuddyEnToken === 'string') snap.codebuddyEnToken = raw.codebuddyEnToken.trim()
702
+ if (typeof raw.endpointOverride === 'string') snap.endpointOverride = raw.endpointOverride.trim()
703
+ if (typeof raw.updatedAt === 'number' && raw.updatedAt > 0) snap.updatedAt = raw.updatedAt
704
+ return snap
705
+ }
706
+
707
+ // 诊断(旧 diagnoseBackend verbatim;fs/env 走 deps)。
708
+ function diagnoseBackend(backend, settings, io) {
709
+ const o = io || {}
710
+ const e = o.env || env
711
+ const s = settings || normalizeSettings(null)
712
+ const authDomain = readBackendAuthDomain(backend, Object.assign({ env: e }, o))
713
+ const override = String(s.endpointOverride || '').trim()
714
+ const byDomain = resolveEndpoint(null, authDomain)
715
+ const endpoint = override || byDomain || BACKEND_ENDPOINTS[backend] || null
716
+ const endpointSource = override ? 'override' : (byDomain ? 'auth-domain' : 'product')
717
+ const tok = resolveEnToken(backend === 'codebuddy-en' ? s.codebuddyEnToken : null, { env })
718
+ return {
719
+ backend: backend,
720
+ authDomain: authDomain,
721
+ endpoint: endpoint,
722
+ endpointSource: endpointSource,
723
+ tokenSource: backend === 'codebuddy-en' ? tok.source : 'not-needed',
724
+ tokenHint: tok.source === 'setting' ? '来自设置面板'
725
+ : tok.source === 'env' ? '来自环境变量 CODEBUDDY_AUTH_TOKEN'
726
+ : tok.source === 'dsh-store' ? '来自 DSH 凭据库(自动复用 WORKBUDDY_TOKEN)'
727
+ : backend === 'codebuddy-en' ? '缺失:国际版 token 不落盘,需在设置面板填写或先在 DSH 配 workbuddy provider'
728
+ : '无需填写(国内版由 CLI 自行登录)',
729
+ mismatch: endpointMismatchHint(backend, authDomain, !!tok.token) !== null,
730
+ hint: endpointMismatchHint(backend, authDomain, !!tok.token) || null,
731
+ defaultModel: String(s.defaultModel || '').trim() || '(CLI 默认)'
732
+ }
733
+ }
734
+
735
+ // ── 结果判定(旧 isLimited/isTransientCliError/failureHint verbatim)─────────
736
+ function isLimitedCodebuddy(res) {
737
+ if (!res || res.ok) return false
738
+ // 前置凭据缺失:配置问题,不是限流(弹窗重试不会让 token 出现)。
739
+ if (res.status === 'AUTH_REQUIRED') return false
740
+ if (res.status === 'SPAWN_ERROR' || res.status === 'CODEBUDDY_UNAVAILABLE' || res.status === 'HUNG_TIMEOUT') return true
741
+ const hay = String(res.stderr || '') + ' ' + String(res.status || '')
742
+ // 401/端点域不匹配:需要用户换登录域,不归限流类(旧 v1.3.0 verbatim)。
743
+ if (/401 authentication failed|differs from the current product endpoint/i.test(hay)) return false
744
+ return CODEBUDDY_LIMIT_RE.test(hay)
745
+ }
746
+
747
+ function isTransientCliError(res) {
748
+ if (!res || res.ok) return false
749
+ if (isLimitedCodebuddy(res)) return false
750
+ return res.status === 'ERROR_DURING_EXECUTION'
751
+ }
752
+
753
+ function failureHint(res) {
754
+ if (!res || res.ok) return ''
755
+ // AUTH_REQUIRED:stderr 已带完整指引,透传(旧 v1.3.1 verbatim)。
756
+ if (res.status === 'AUTH_REQUIRED') return '[诊断] ' + String(res.stderr || '')
757
+ if (isTransientCliError(res)) {
758
+ return res.retried
759
+ ? '[诊断] codebuddy CLI 侧瞬时错误(error_during_execution,CLI 未给出原因),已自动重试 1 次仍失败。可换 model 再试一次;若仍失败请改用原生工具完成,或告知用户。'
760
+ : '[诊断] codebuddy CLI 侧瞬时错误(error_during_execution,CLI 未给出原因)。直接重试同一请求通常即可通过;也可显式指定 model。不要据此认为任务本身有问题。'
761
+ }
762
+ if (res.status === 'PARSE_ERROR') {
763
+ return '[诊断] 未能从 codebuddy 输出解析出 result 事件(进程可能被中断或输出被截断)。可重试;持续出现请检查 codebuddy CLI 版本。'
764
+ }
765
+ if (/401 authentication failed|differs from the current product endpoint/i.test(String(res.stderr || ''))) {
766
+ return '[诊断] 认证失败(401):请求被送到的端点与当前 CLI 登录域不匹配(国际/国内登录互斥)。请让用户在对应产品域完成 `codebuddy` CLI 登录后重试;这不是限流,重试与回退都无效。'
767
+ }
768
+ return ''
769
+ }
770
+
771
+ // stream-json 解析(旧 parseCodebuddyJson verbatim)。
772
+ function parseCodebuddyJson(stdoutText) {
773
+ const trimmed = String(stdoutText || '').trim()
774
+ if (!trimmed) return null
775
+ const lines = trimmed.split(/\r?\n/).filter(Boolean)
776
+ for (let i = lines.length - 1; i >= 0; i--) {
777
+ const ln = lines[i].trim()
778
+ if (!ln.startsWith('{')) continue
779
+ try {
780
+ const obj = JSON.parse(ln)
781
+ if (obj && obj.type === 'result') return obj
782
+ } catch (e) {}
783
+ }
784
+ try { return JSON.parse(trimmed) } catch (e) {}
785
+ return null
786
+ }
787
+
788
+ // result 事件 → 统一结果对象(旧 buildResult verbatim,含账号清单收割)。
789
+ function buildResult(parsed, outcome, mode, stderrText, stdoutText, backend) {
790
+ const bk = backend || 'codebuddy'
791
+ const exitCode = outcome ? outcome.exitCode : null
792
+ const errText = parsed && typeof parsed.error === 'string' && parsed.error ? parsed.error : ''
793
+ const stderr = (stderrText ? String(stderrText).slice(-2000) : '') + (errText ? (stderrText ? ' ' : '') + errText : '')
794
+ // v1.6.0:顺手收割服务端回吐的「本账号可用模型」清单(失败静默)。
795
+ try {
796
+ const harvest = parseAccountModels(stderr) || parseAccountModels(String(stdoutText || ''))
797
+ if (harvest && harvest.length) rememberAccountCatalog(bk, harvest)
798
+ } catch (e) { /* 收割失败不影响主流程 */ }
799
+ if (parsed && parsed.type === 'result') {
800
+ const isOk = exitCode === 0 && parsed.is_error === false && parsed.subtype === 'success'
801
+ let modelUsed = null
802
+ try {
803
+ if (parsed.modelUsage && typeof parsed.modelUsage === 'object') {
804
+ const names = Object.keys(parsed.modelUsage)
805
+ if (names.length) modelUsed = names.join('+')
806
+ }
807
+ } catch (e) { }
808
+ return {
809
+ ok: isOk,
810
+ status: String(parsed.subtype || (isOk ? 'SUCCESS' : 'ERROR')).toUpperCase(),
811
+ response: typeof parsed.result === 'string' ? parsed.result : '',
812
+ sessionId: typeof parsed.session_id === 'string' ? parsed.session_id : null,
813
+ durationSeconds: typeof parsed.duration_ms === 'number' ? parsed.duration_ms / 1000 : null,
814
+ numTurns: typeof parsed.num_turns === 'number' ? parsed.num_turns : null,
815
+ totalTokens: (parsed.usage && typeof parsed.usage.input_tokens === 'number' && typeof parsed.usage.output_tokens === 'number')
816
+ ? (parsed.usage.input_tokens + parsed.usage.output_tokens) : null,
817
+ exitCode: exitCode,
818
+ mode: mode,
819
+ backend: bk,
820
+ modelUsed: modelUsed,
821
+ stderr: stderr
822
+ }
823
+ }
824
+ return {
825
+ ok: false,
826
+ status: 'PARSE_ERROR',
827
+ response: '',
828
+ sessionId: null,
829
+ durationSeconds: null,
830
+ numTurns: null,
831
+ totalTokens: null,
832
+ exitCode: exitCode,
833
+ mode: mode,
834
+ backend: bk,
835
+ stderr: stderr,
836
+ rawStdout: String(stdoutText || '').slice(-2000)
837
+ }
838
+ }
839
+
840
+ // 统一 argv 构造(旧 buildArgv verbatim;env 端点/凭据注入在本 provider 内完成,
841
+ // 因为 kit 只透传 opts,宿主三形态都从本函数的返回值取 built.env 下发 spawn)。
842
+ function buildArgvCodebuddy(prefix, args, opts) {
843
+ const o = opts || {}
844
+ const argv = [prefix[0], ...prefix.slice(1), '-p', String(args.prompt), '--output-format', 'stream-json']
845
+ let mode = args.mode || o.defaultMode || 'auto'
846
+ if (mode === 'auto') mode = o.planActive ? 'plan' : 'bypassPermissions'
847
+ if (mode === 'plan') {
848
+ argv.push('--permission-mode', 'plan')
849
+ argv.push('--allowedTools', 'Bash')
850
+ } else {
851
+ mode = 'bypassPermissions'
852
+ argv.push('--permission-mode', 'bypassPermissions')
853
+ }
854
+ // 模型优先级:显式 args.model > 用户偏好 defaultModel > CLI 默认。
855
+ const model = args.model || o.defaultModel
856
+ if (model) argv.push('--model', String(model))
857
+ if (args.effort) argv.push('--effort', String(args.effort))
858
+ if (args.maxTurns !== undefined && args.maxTurns !== null) argv.push('--max-turns', String(clampInt(args.maxTurns, 50, 1, 500)))
859
+ if (Array.isArray(args.addDirs)) for (const d of args.addDirs) { if (d) argv.push('--add-dir', String(d)) }
860
+ if (args.sessionId) argv.push('--resume', String(args.sessionId))
861
+ else if (args.continueLatest) argv.push('--continue')
862
+ const timeoutSec = clampInt(args.timeoutSec, 300, 10, 3600)
863
+ // 端点/凭据注入(旧 preset/dynamic/MCP 三形态一致语义,宿主不必自算):
864
+ // authDomain 只在有 fs 能力的宿主读得到;dynamic 沙箱 → 沿用 CLI 自身端点。
865
+ const bk = o.backend || DEFAULT_BACKEND
866
+ const settings = o.settings || null
867
+ const authDomain = readBackendAuthDomain(bk, { env })
868
+ const tok = resolveEnToken(bk === 'codebuddy-en' && settings ? settings.codebuddyEnToken : null, { env })
869
+ const inject = endpointEnv(bk, settings ? settings.endpointOverride : null, authDomain, tok.token)
870
+ return { argv: argv, timeoutSec: timeoutSec, mode: mode, env: o.env || inject }
871
+ }
872
+
873
+ // 流事件折叠(旧 foldEvent 主体 verbatim;project 状态由 kit engine 持有)。
874
+ function foldStreamEvent(ev, p, summarizeArgs, MAX_ARG_LEN, MAX_TRAIL) {
875
+ if (!ev || !ev.type) return
876
+ if (ev.type === 'assistant' && ev.message && Array.isArray(ev.message.content)) {
877
+ for (const block of ev.message.content) {
878
+ if (block.type === 'tool_use') {
879
+ p._stepCounter = (p._stepCounter || 0) + 1
880
+ const stepIndex = p._stepCounter
881
+ if (!p._toolMap) p._toolMap = {}
882
+ p._toolMap[block.id] = { stepIndex: stepIndex, name: block.name }
883
+ const args = summarizeArgs(block.input)
884
+ p.trail.push({ stepIndex: stepIndex, state: 'ACTIVE', tool: block.name, args: args, at: new Date().toISOString() })
885
+ if (p.trail.length > MAX_TRAIL) p.trail.shift()
886
+ p.current = { tool: block.name, args: args, stepIndex: stepIndex, since: new Date().toISOString() }
887
+ } else if (block.type === 'thinking') {
888
+ p.current = { tool: 'thinking', args: { text: String(block.thinking || '').slice(0, MAX_ARG_LEN) }, stepIndex: 0, since: new Date().toISOString() }
889
+ } else if (block.type === 'text') {
890
+ p.current = { tool: 'typing', args: { text: String(block.text || '').slice(0, MAX_ARG_LEN) }, stepIndex: 0, since: new Date().toISOString() }
891
+ }
892
+ }
893
+ } else if (ev.type === 'user' && ev.message && Array.isArray(ev.message.content)) {
894
+ for (const block of ev.message.content) {
895
+ if (block.type === 'tool_result' && p._toolMap && p._toolMap[block.tool_use_id]) {
896
+ const rec = p._toolMap[block.tool_use_id]
897
+ p.trail.push({ stepIndex: rec.stepIndex, state: block.is_error ? 'ERROR' : 'DONE', tool: rec.name, args: null, at: new Date().toISOString() })
898
+ if (p.trail.length > MAX_TRAIL) p.trail.shift()
899
+ if (p.current && p.current.stepIndex === rec.stepIndex) p.current = null
900
+ }
901
+ }
902
+ }
903
+ }
904
+
905
+ const descriptor = {
906
+ id: 'codebuddy',
907
+ label: 'CB',
908
+ presetId: 'codebuddy-first',
909
+ defaultBackend: DEFAULT_BACKEND,
910
+ defaultMode: 'auto',
911
+ exeName: 'codebuddy',
912
+ binEnvVars: ['CODEBUDDY_BIN', 'CODEBUDDY_EN_BIN', 'WORKBUDDY_BIN'],
913
+ unavailableStatus: 'CODEBUDDY_UNAVAILABLE',
914
+ // 旧 renderResult verbatim 语义:标头用具体 backend('workbuddy OK'),仅当结果
915
+ // 无 backend 字段时回落 'codebuddy'(= provider.id)。agy/mimo 旧包标头硬编码
916
+ // provider 名,不设此旗。
917
+ backendLabel: true,
918
+
919
+ backends: () => BACKENDS.slice(),
920
+ normalizeBackend,
921
+ resolveBackend,
922
+
923
+ buildArgv: buildArgvCodebuddy,
924
+ parseJson: parseCodebuddyJson,
925
+ buildResult,
926
+ foldStreamEvent,
927
+ isLimited: isLimitedCodebuddy,
928
+ isTransientCliError,
929
+ failureHint,
930
+
931
+ // codebuddy-en 无凭据的确定性前置(v1.3.1 语义,经 kit preflight 钩子实现):
932
+ // 三通道皆空 → 直接 AUTH_REQUIRED(可操作指引进 stderr),不白跑一次 spawn。
933
+ // 其它后端一律放行。dynamic 沙箱无 fs → dsh-store 通道缺席,abort 提示即可。
934
+ preflight({ args, backend, engine, o }) {
935
+ if (backend !== 'codebuddy-en') return null
936
+ const settings = (o && o.getSettings && o.getSettings()) || null
937
+ const tok = resolveEnToken(settings ? settings.codebuddyEnToken : null, { env })
938
+ if (tok.token) return null
939
+ const authDomain = readBackendAuthDomain('codebuddy-en', { env })
940
+ const hint = endpointMismatchHint('codebuddy-en', authDomain, false)
941
+ return {
942
+ abort: {
943
+ ok: false,
944
+ status: 'AUTH_REQUIRED',
945
+ response: '',
946
+ sessionId: null,
947
+ durationSeconds: null,
948
+ numTurns: null,
949
+ totalTokens: null,
950
+ exitCode: null,
951
+ mode: (args && args.mode) || descriptor.defaultMode || 'auto',
952
+ backend: 'codebuddy-en',
953
+ stderr: hint
954
+ }
955
+ }
956
+ },
957
+
958
+ tools: {
959
+ run: {
960
+ name: 'codebuddy_run',
961
+ description: 'Dispatch a coding/build/debug/investigation task to the local codebuddy agent CLI and return its final answer. Prefer this for implementation, edits, refactors, multi-file investigation and debugging in every mode. DSH fully controls codebuddy: it always runs non-interactively with permissions auto-approved (codebuddy never prompts). Use read-only native tools only for quick lookups and for final build/test verification. In mode=auto the DSH plan state decides plan vs bypassPermissions. Set background=true for long tasks and collect the result via job_output.',
962
+ inputSchema: RUN_PARAMS
963
+ },
964
+ continue: {
965
+ name: 'codebuddy_continue',
966
+ description: 'Continue an existing codebuddy conversation with a follow-up prompt, reusing codebuddy context. Pass sessionId from a prior codebuddy_run result, or set latest=true to continue the most recent codebuddy conversation. Same DSH-controlled, no-prompt execution as codebuddy_run.',
967
+ inputSchema: CONTINUE_PARAMS
968
+ },
969
+ status: {
970
+ name: 'codebuddy_status',
971
+ description: 'Read a live snapshot of what the local codebuddy agent is currently doing. Returns one section per project (working directory): running count, the current step (tool name + arguments being executed, or agent_response thinking/typing), the recent step trail (tools executed, done/error), the last completed run status + session id, and per-project cumulative usage (runs + total tokens, since codebuddy exposes no quota API). Optional cwd filters to a single project. Call this to check on an in-flight codebuddy_run/codebuddy_continue without waiting for it to finish.',
972
+ inputSchema: STATUS_PARAMS
973
+ }
974
+ },
975
+
976
+ // codebuddy_models:只在宿主有 spawn/fs 能力时注册(preset 注 deps.process;MCP 注
977
+ // deps.nodeChild;dynamic 沙箱省略)。**失败必须 ok:false + 可操作错误**,绝不静默返回
978
+ // 空列表或伪造模型(红线 §7;对照 workbuddy-auth 的 catalog-store 静默降级 —— 不照抄)。
979
+ extraTools: (deps.process || deps.nodeChild || deps.nodeFs) ? [codebuddyModelsTool({
980
+ env, deps, BACKEND_ENUM, normalizeBackend, resolveBackend, modelCatalogDetailed, getAccountCatalog,
981
+ // faceEnv:让账号探测打到**请求的那个产品面**(与 buildArgv 同一套端点/凭据注入)。
982
+ // 缺了它探测会走 CLI 自身默认登录面 —— 标着 codebuddy-en 却去问 codebuddy 面。
983
+ faceEnv: (backend) => {
984
+ try {
985
+ const settings = (deps.settings && typeof deps.settings === 'object') ? deps.settings : {}
986
+ const authDomain = readBackendAuthDomain(backend, { env })
987
+ const tok = resolveEnToken(backend === 'codebuddy-en' ? settings.codebuddyEnToken : null, { env })
988
+ return endpointEnv(backend, settings.endpointOverride, authDomain, tok.token) || {}
989
+ } catch (e) { return {} }
990
+ }
991
+ })] : [],
992
+
993
+ // 设置分区(契约 §3):keystone 统一文件 {codebuddy:{...}};旧扁平文件的字段
994
+ // 级迁移由 keystone LEGACY_SOURCES 承担(字段名逐字相同)。
995
+ settings: {
996
+ keys: SETTINGS_KEYS,
997
+ secretKeys: ['codebuddyEnToken'],
998
+ defaults: () => normalizeSettings(null),
999
+ normalize: (raw) => normalizeSettings(raw),
1000
+ meta() {
1001
+ // defaultModel 下拉 = 缓存里的账号级清单。走过的弯路:静态表层(16 项、2 个 id 已改名)
1002
+ // → CLI `--help` 名册(18 项但仍滞后、含已废 id)→ 无效模型探测让服务端回权威清单。
1003
+ // 契约 §3.2 起 meta() **只读缓存、绝不 spawn**;探测由「拉取可用模型」触发 refreshModels。
1004
+ // 面的取值必须与 refreshModels 同源(先宿主设置、再设置文件),否则会「刷新成功但下拉仍空」。
1005
+ const face0 = (deps.settings && deps.settings.preferredBackend) || currentPreferredBackend()
1006
+ const face = normalizeBackend(face0) || DEFAULT_BACKEND
1007
+ const live = cachedCliModels(face) // 只读:未命中返回 undefined
1008
+ let options = []
1009
+ let note = '点下方「拉取可用模型」从服务端获取本账号的可用清单;当前面:' + face +
1010
+ '。日常任务优先免费(x0.00)。'
1011
+ if (live && live.ids && live.ids.length) {
1012
+ const detail = modelCatalogDetailed(face, { env, accountModels: live.ids })
1013
+ const byId = new Map(detail.map((x) => [x.id, x]))
1014
+ options = live.ids.map((id) => {
1015
+ const d = byId.get(id)
1016
+ return { value: id, label: (d && d.label) || id }
1017
+ })
1018
+ note = '上次拉取结果(' + live.ids.length + ' 项,服务端裁决);当前面:' + face +
1019
+ '。要更新请重新点「拉取可用模型」。'
1020
+ } else {
1021
+ note = '尚未拉取(或该面未登录)。点「拉取可用模型」让服务端回吐本账号的清单;' +
1022
+ '此前也可直接手输模型 id。当前面:' + face + '。'
1023
+ }
1024
+ return {
1025
+ titleSuffix: 'CodeBuddy',
1026
+ fields: [
1027
+ {
1028
+ key: 'preferredBackend', label: '默认后端', type: 'select',
1029
+ options: BACKENDS.map((b) => ({ value: b, label: BACKEND_LABELS[b] })),
1030
+ note: '新会话未显式指定 backend 时用这里选的后端;单次调用可覆盖。codebuddy-en 需要 codebuddyEnToken 或 DSH 已配 workbuddy key。'
1031
+ },
1032
+ {
1033
+ key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true, placeholder: '(CLI 默认)',
1034
+ options,
1035
+ refresh: { label: '拉取可用模型' },
1036
+ note
1037
+ },
1038
+ { key: 'codebuddyEnToken', label: 'WorkBuddy 国际版 Token', type: 'secret', note: '仅 backend="codebuddy-en" 需要。国际版登录 token 被 protector key 封装不落盘,只能手工提供;留空则自动尝试 DSH 凭据库的 WORKBUDDY_TOKEN。' },
1039
+ { key: 'endpointOverride', label: '端点覆盖', type: 'text', placeholder: 'https://…/v2', note: '一般留空:端点按登录域自动对齐(www.codebuddy.cn 等)。仅在需要强制指定 API 端点时填写,且必须自带 /v2。' }
1040
+ ]
1041
+ }
1042
+ },
1043
+ diagnose(snap) {
1044
+ const s = normalizeSettings(snap)
1045
+ const lines = []
1046
+ const seen = new Set()
1047
+ // 默认后端一行;codebuddy-en 是 4 面里唯一需要凭据的,恒附一行状态。
1048
+ for (const b of [s.preferredBackend, 'codebuddy-en']) {
1049
+ if (seen.has(b)) continue
1050
+ seen.add(b)
1051
+ const d = diagnoseBackend(b, s, {})
1052
+ lines.push(b + ': ' + (d.endpoint || '?') + '(登录域 ' + (d.authDomain || '未读到') + ',' + d.tokenHint + ')')
1053
+ if (d.hint) lines.push(' ⚠ ' + d.hint)
1054
+ }
1055
+ return lines.join('\n')
1056
+ },
1057
+ /**
1058
+ * 手动拉取(契约 §3.2):唯一权威源 = 无效模型触发的服务端 400 报文(「Currently
1059
+ * supported models for your account」)。成功后回填缓存,settingsView 再取 meta 即显示。
1060
+ * 失败一律 ok:false + 可操作错误 —— 绝不返回 {ok:true, ids:[]},也绝不把静态表/CLI
1061
+ * 自报名册冒充成账号可用清单(0.2.6~0.2.7 反复踩过的坑)。
1062
+ */
1063
+ async refreshModels(settings, envOverride) {
1064
+ const s = normalizeSettings(settings)
1065
+ const face = normalizeBackend(s.preferredBackend) || DEFAULT_BACKEND
1066
+ const e = envOverride || env
1067
+ // 端点/凭据注入与 buildArgv 同一套规则(否则会拿 A 面凭据去问 B 面)。
1068
+ let faceEnv = {}
1069
+ try {
1070
+ const authDomain = readBackendAuthDomain(face, { env: e })
1071
+ const tok = resolveEnToken(face === 'codebuddy-en' ? s.codebuddyEnToken : null, { env: e })
1072
+ faceEnv = endpointEnv(face, s.endpointOverride, authDomain, tok.token) || {}
1073
+ } catch (err) { faceEnv = {} }
1074
+ let probe
1075
+ try {
1076
+ probe = await probeAccountModels(e, { envExtra: faceEnv, timeoutMs: 90000 })
1077
+ } catch (err) {
1078
+ return { ok: false, ids: [], source: 'account-live', error: '探测进程异常:' + String((err && err.message) || err) }
1079
+ }
1080
+ if (!probe || !probe.ok || !probe.ids || !probe.ids.length) {
1081
+ return {
1082
+ ok: false, ids: [], source: 'account-live',
1083
+ error: '未能从「' + face + '」面获取账号模型清单:' + String((probe && probe.error) || 'no roster') +
1084
+ '。常见原因:该面未登录(先在 codebuddy CLI 里 /login)、token 缺失或端点不通;' +
1085
+ '也可在「默认后端」里换个面再拉。'
1086
+ }
1087
+ }
1088
+ rememberCliModels(face, probe.ids, 'account-live')
1089
+ // 标签沿用目录展示名(没有就回落 id),与下拉其它项一致。
1090
+ const detail = modelCatalogDetailed(face, { env: e, accountModels: probe.ids })
1091
+ const labels = {}
1092
+ for (const x of (Array.isArray(detail) ? detail : [])) if (x && x.label) labels[x.id] = x.label
1093
+ return { ok: true, ids: probe.ids, labels, source: 'account-live' }
1094
+ }
1095
+ },
1096
+
1097
+ mcp: { version: '0.2.2' }
1098
+ }
1099
+
1100
+ // 三形态共用的工具逻辑(签名保持 keystone/队友接线契约不变)。
1101
+ descriptor.runToolLogic = async function (args, exec) {
1102
+ return this.runner.coreExecute(args, exec)
1103
+ }
1104
+ // 旧 preset continueTool.execute verbatim 映射(sessionId 优先,否则 latest)。
1105
+ descriptor.continueToolLogic = async function (args, exec) {
1106
+ const a = args || {}
1107
+ const mapped = { prompt: a.prompt, backend: a.backend, mode: a.mode, model: a.model, effort: a.effort, maxTurns: a.maxTurns, cwd: a.cwd, timeoutSec: a.timeoutSec, background: a.background }
1108
+ if (a.sessionId) mapped.sessionId = a.sessionId
1109
+ else if (a.latest) mapped.continueLatest = true
1110
+ return this.runner.coreExecute(mapped, exec)
1111
+ }
1112
+ // 旧 statusTool.execute verbatim:cwd 过滤 + 单项目聚合覆写(含 runs/totalTokens)。
1113
+ descriptor.statusToolLogic = function (args) {
1114
+ const a = args || {}
1115
+ const snap = this.engine.statusSnapshot()
1116
+ if (a.cwd) {
1117
+ const key = String(a.cwd)
1118
+ snap.projects = snap.projects.filter((p) => p.cwd === key)
1119
+ const g = snap.projects[0]
1120
+ if (g) { snap.state = g.state; snap.running = g.running; snap.current = g.current; snap.trail = g.trail; snap.lastStatus = g.lastStatus; snap.lastAt = g.lastAt; snap.lastSessionId = g.lastSessionId; snap.lastBackend = g.lastBackend; snap.fallbackActive = g.fallbackActive; snap.runs = g.runs; snap.totalTokens = g.totalTokens; snap.updatedAt = g.updatedAt }
1121
+ }
1122
+ return snap
1123
+ }
1124
+
1125
+ // 供 preset/dynamic/MCP/测试形态复用的领域函数(不 import home-plugin,全部经
1126
+ // 描述符暴露;MCP 面据 buildArgv/parseJson/buildResult/isLimited/failureHint 组
1127
+ // 装真实 spawn 执行,语义与旧 mcp/codebuddy-mcp-server.mjs 一致)。
1128
+ Object.assign(descriptor, {
1129
+ policyText: POLICY_TEXT, limits: { LIMIT_RE: CODEBUDDY_LIMIT_RE },
1130
+ backendLabels: BACKEND_LABELS, backendEndpoints: BACKEND_ENDPOINTS,
1131
+ authDomainEndpoints: AUTH_DOMAIN_ENDPOINTS, backendAuthIds: BACKEND_AUTH_IDS,
1132
+ staticModelIds: BACKEND_MODEL_IDS, endpointHost, resolveEndpoint, endpointEnv,
1133
+ endpointMismatchHint, resolveEnToken, readDshWorkbuddyToken, readBackendAuthDomain,
1134
+ diagnoseBackend, parseAccountModels, rememberAccountCatalog, getAccountCatalog,
1135
+ normalizeProductConfig, extractModelIds, extractModelCredits, parseCreditValue,
1136
+ formatCreditLabel, lookupModelCredits, normalizeSettings
1137
+ })
1138
+ // env 经 io 绑定:catalog 是模块级纯函数,若共用模块级 env,多 provider 实例会互相
1139
+ // 覆盖(测试串扰)。这里每个实例调用时都带上自己的 env。
1140
+ const withEnv = (fn) => (backend, io) => fn(backend, Object.assign({ env }, io || {}))
1141
+ Object.assign(descriptor, {
1142
+ backendModelCatalog: withEnv(backendModelCatalog),
1143
+ modelCatalogDetailed: withEnv(modelCatalogDetailed),
1144
+ formatModelCatalogText: withEnv(formatModelCatalogText)
1145
+ })
1146
+
1147
+ descriptor.attach = function (hostOpts) {
1148
+ host = hostOpts || {}
1149
+ this.engine = createStatusEngine({ provider: this, publish: host.publish })
1150
+ const runner = createRunner(Object.assign({}, host, { provider: this, engine: this.engine }))
1151
+ this.runner = runner
1152
+ return runner
1153
+ }
1154
+ descriptor.hostOptions = () => host
1155
+ return descriptor
1156
+ }
1132
1157
 
1133
1158
  // ── inlined from providers/codebuddy-catalog.mjs ──
1134
- // providers/codebuddy-catalog.mjs — codebuddy 模型目录/积分/描述文件解析(纯逻辑)。
1135
- //
1136
- // 从 providers/codebuddy.mjs 拆出:主文件原 864 行远超 600 行红线(规则 §26)。
1137
- // 只放「模型目录相关」纯逻辑:静态表、描述文件候选路径、模型 id 抽取、积分倍率、
1138
- // 账号清单收割、目录渲染。不含 CLI 调用、状态机、凭据链。
1139
- //
1140
- // 沙箱纪律(与 providers/*.mjs 同):本文件会被 scripts/build.mjs 经模板顶部的
1141
- // 第三个占位标记内联进同一个 dynamic 沙箱脚本作用域 —— 所以:
1142
- // 1) 不得 import 任何模块(import 行会被 stripModuleSyntax 剥掉,函数体留下但
1143
- // 依赖丢失 → 运行期 ReferenceError);node 内建走 cbCatalog* 守卫;
1144
- // 2) 顶层标识符不得与 bridge-kit / codebuddy.mjs 剩余部分撞名(同一作用域重复
1145
- // 声明 = 构建期 Identifier already declared);
1146
- // 3) 不得出现 import.meta 字面量;
1147
- // 4) ⚠️ 注释里绝不写出那个占位标记的字面量:build 做全局文本替换,注释中出现
1148
- // 同样会被替换成正文代码,把注释从中间劈开 → 生成物语法崩。
1149
-
1150
- // ── 宿主能力注入(env / fs / path / sep)───────────────────────────────────
1151
- // 本模块函数搬自主文件的闭包,拿不到那边的 env / fsRead / pathJoin / pathSep。
1152
- // 由 codebuddy.mjs 在 defineProvider 里调 cbCatalogSetHost() 注入;未注入时退到
1153
- // getBuiltinModule 守卫(沙箱里没有 → 返回 null / 默认分隔符,绝不抛)。
1154
- // 名字一律带 cbCatalog_ 前缀:内联后与主文件的 fsRead/pathJoin 同处一个作用域。
1155
- let cbCatalogEnv = {}
1156
- let cbCatalogFs = null
1157
- let cbCatalogPathSep = "/"
1158
-
1159
- function cbCatalogSetHost(envObj, pathMod, fsMod) {
1160
- if (envObj && typeof envObj === "object") cbCatalogEnv = envObj
1161
- if (fsMod && typeof fsMod.readFileSync === "function") cbCatalogFs = fsMod
1162
- if (pathMod && typeof pathMod.sep === "string") cbCatalogPathSep = pathMod.sep
1163
- else if (/[A-Za-z]:[\\/]/.test(String((cbCatalogEnv && (cbCatalogEnv.USERPROFILE || cbCatalogEnv.APPDATA)) || ""))) cbCatalogPathSep = "\\"
1164
- }
1165
-
1166
- function cbCatalogRead(p, enc) {
1167
- if (!p) return null
1168
- try {
1169
- if (cbCatalogFs && typeof cbCatalogFs.readFileSync === "function") return cbCatalogFs.readFileSync(p, enc || "utf8")
1170
- const proc = globalThis.process
1171
- const nf = (proc && typeof proc.getBuiltinModule === "function") ? proc.getBuiltinModule("node:fs") : null
1172
- if (!nf || typeof nf.readFileSync !== "function") return null
1173
- return nf.readFileSync(p, enc || "utf8")
1174
- } catch (e) { return null }
1175
- }
1176
-
1177
- function cbCatalogPathJoin(...parts) {
1178
- try {
1179
- const proc = globalThis.process
1180
- const np = (proc && typeof proc.getBuiltinModule === "function") ? proc.getBuiltinModule("node:path") : null
1181
- if (np && typeof np.join === "function") return np.join(...parts)
1182
- } catch (e) { }
1183
- return parts.join("/")
1184
- }
1185
-
1186
- // ── 以下常量与函数原样搬自 providers/codebuddy.mjs(行为 zero-change)────────
1187
-
1188
- const BACKEND_MODEL_IDS = {
1189
- 'codebuddy': ['hy4-preview', 'hy3', 'hy3-x', 'deepseek-v4.1-flash', 'glm-5.3', 'glm-5.3-flash', 'glm-5.2', 'glm-5.1', 'glm-5v-turbo', 'minimax-m3', 'minimax-m2.7', 'kimi-k3-1', 'kimi-k2.8-preview', 'kimi-k2.7', 'kimi-k2.6', 'deepseek-v4-pro'],
1190
- 'codebuddy-intl': ['claude-sonnet-5', 'claude-sonnet-5-1m', 'claude-opus-5', 'claude-opus-4.8', 'claude-opus-4.8-1m', 'gemini-3.1-pro', 'gemini-3.5-flash', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.5', 'gpt-5.4', 'glm-5.3-ioa', 'glm-5.3-flash-ioa', 'glm-5.2-ioa', 'kimi-k3-ioa', 'kimi-k2.8-preview', 'kimi-k2.6-ioa', 'minimax-m3-ioa', 'minimax-m2.7-ioa', 'deepseek-v4.1-flash', 'deepseek-v4-pro-ioa'],
1191
- // WorkBuddyAI 自制目录(product.json 顶层 37 项,按其 credits 排序:免费的在前)
1192
- 'codebuddy-en': ['hy3', 'gpt-5.1-codex-mini', 'gemini-3.1-flash-lite', 'gemini-2.5-flash', 'minimax-m3', 'gemini-3.0-flash', 'fast-model', 'deepseek-v3-2-volc', 'balanced-model', 'kimi-k2.5', 'kimi-k2.6', 'default-model-lite', 'glm-5.3', 'glm-5.2', 'glm-5.0', 'gpt-5.1-codex', 'gemini-2.5-pro', 'gemini-3.5-flash', 'gpt-5.3-codex', 'gemini-3.1-pro', 'gpt-5.6-terra', 'kimi-k3', 'gpt-5.4', 'primary-model', 'default-model', 'deep-model', 'gpt-5.5', 'gpt-5.6-sol', 'gpt-5.6-luna'],
1193
- // WorkBuddy 国内桌面自制目录(product.json:applicationName=WorkBuddy)
1194
- 'workbuddy': ['auto', 'hy4-preview', 'hy3', 'hy3-x', 'deepseek-v4.1-flash', 'glm-5.3', 'glm-5.3-flash', 'glm-5.2', 'glm-5.1', 'glm-5v-turbo', 'minimax-m3', 'minimax-m2.7', 'kimi-k3-1', 'kimi-k2.8-preview', 'kimi-k2.7', 'kimi-k2.6', 'deepseek-v4-pro']
1195
- }
1196
-
1197
- // 各后端真正生效的 product 描述文件位置(旧 core v1.6.1 verbatim)。
1198
- const PRODUCT_DESCRIPTOR_ENV = {
1199
- 'codebuddy': { env: 'CODEBUDDY_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: null },
1200
- 'codebuddy-intl': { env: 'CODEBUDDY_INTL_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: null },
1201
- 'codebuddy-en': { env: 'CODEBUDDY_EN_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: '.workbuddy-ai' },
1202
- 'workbuddy': { env: 'WORKBUDDY_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: '.workbuddy' }
1203
- }
1204
-
1205
- // 倍率跨命名匹配(账号清单 id → 描述文件 id,旧 core verbatim)。
1206
- const CREDIT_ID_ALIASES = {
1207
- 'kimi-k3-1': ['kimi-k3', 'kimi-k3-ioa'],
1208
- 'hy3-x': ['hy3', 'hy3-ioa'],
1209
- 'default': ['codewise-default-model-v2'],
1210
- 'auto': ['hy3', 'hy3-ioa']
1211
- }
1212
-
1213
- const MODEL_DESCRIPTION = 'Optional model id. When unspecified, the user-preferred default model (plugin settings) is used if set, else the CLI default. Two layers decide: the PRODUCT FACE decides whether the id exists in its catalogue, then the ACCOUNT decides whether it is licensed. The free tier (hy3, deepseek-v4.1-flash at x0.00 credits; hy4-preview at x0.29) belongs to the npm CLI faces codebuddy/codebuddy-intl: hy4-preview, hy3, hy3-x, deepseek-v4.1-flash, glm-5.3, glm-5.3-flash, glm-5.2, glm-5.1, glm-5v-turbo, minimax-m3, minimax-m2.7, kimi-k3-1, kimi-k2.8-preview, kimi-k2.7, kimi-k2.6, deepseek-v4-pro. codebuddy-en (WorkBuddyAI) carries NO hy4-preview and NO deepseek-v4.1-flash; its only free model is hy3. International ids (claude-sonnet-5, gemini-3.1-pro, ...) are account-gated. Availability is per ACCOUNT: the CLI rejects an id outside the account catalogue with "400 service info not found" and one inside it but unlicensed with "400 only available for authorized users" (many catalogue flagships such as claude-opus-4.8, gpt-5.4 and gpt-6-astra are in this second group for a domestic account). Run the CLI with a deliberately invalid model id to print "Currently supported models for your account:" and get the authoritative list; the plugin caches that result and the settings panel lists exactly those ids with their credit multiplier (x0.00 = free), read live from the CLI descriptor so it stays in sync with CLI upgrades. Prefer the cheapest model that can do the job, and prefer a free (x0.00) one for routine work. Pass a model only when the task clearly benefits from a specific one.'
1214
-
1215
- const BACKEND_ENUM = ['codebuddy', 'codebuddy-intl', 'codebuddy-ioa', 'codebuddy-international', 'codebuddy-en', 'workbuddy-en', 'workbuddy-ai', 'workbuddy']
1216
-
1217
- function accountConfigCandidates(backend, io) {
1218
- const e = (io && io.env) || cbCatalogEnv
1219
- const spec = PRODUCT_DESCRIPTOR_ENV[backend]
1220
- if (!spec || !spec.accountDir) return []
1221
- const home = e.USERPROFILE || e.HOME
1222
- if (!home) return []
1223
- const base = cbCatalogPathJoin(home, spec.accountDir.split('/').join(cbCatalogPathSep))
1224
- return [
1225
- cbCatalogPathJoin(base, 'cache', 'acc-product-config-v3.json'),
1226
- cbCatalogPathJoin(base, 'local_storage', 'entry_d43e96994f944cfb77961c2ea7d04605.info'),
1227
- cbCatalogPathJoin(base, 'local_storage', 'wb_entry_d43e96994f944cfb77961c2ea7d04605.info')
1228
- ]
1229
- }
1230
-
1231
- function productDescriptorCandidates(backend, io) {
1232
- const e = (io && io.env) || cbCatalogEnv
1233
- const spec = PRODUCT_DESCRIPTOR_ENV[backend]
1234
- if (!spec) return []
1235
- const out = []
1236
- for (const p of accountConfigCandidates(backend, io)) out.push(p)
1237
- if (e[spec.env]) out.push(e[spec.env])
1238
- const rels = spec.fallbackRel ? [spec.rel, spec.fallbackRel] : [spec.rel]
1239
- const push = (root) => { if (root) for (const r of rels) out.push(cbCatalogPathJoin(root, ...r.split('/'))) }
1240
- if (backend === 'codebuddy' || backend === 'codebuddy-intl') {
1241
- push(e.APPDATA ? cbCatalogPathJoin(e.APPDATA, 'npm', 'node_modules', '@tencent-ai', 'codebuddy-code') : null)
1242
- } else if (backend === 'codebuddy-en') {
1243
- push('C:\\Program Files\\WorkBuddyAI\\resources\\app.asar.unpacked\\cli')
1244
- } else if (backend === 'workbuddy') {
1245
- push('C:\\Program Files\\WorkBuddy\\resources\\app.asar.unpacked\\cli')
1246
- }
1247
- return out
1248
- }
1249
-
1250
- function normalizeProductConfig(raw) {
1251
- try {
1252
- const obj = typeof raw === 'string' ? JSON.parse(raw) : raw
1253
- if (!obj) return null
1254
- if (Array.isArray(obj)) {
1255
- // 数组形态:从后往前找第一个带 data 的条目(末尾最新)。
1256
- for (let i = obj.length - 1; i >= 0; i--) {
1257
- const e = obj[i]
1258
- if (e && typeof e === 'object' && e.data && typeof e.data === 'object') return e.data
1259
- }
1260
- return null
1261
- }
1262
- if (typeof obj !== 'object') return null
1263
- if (obj.data && typeof obj.data === 'object' && !Array.isArray(obj.models) && !Array.isArray(obj.agents)) return obj.data
1264
- return obj
1265
- } catch (e) { return null }
1266
- }
1267
-
1268
- function extractModelIds(productJson) {
1269
- try {
1270
- const obj = normalizeProductConfig(productJson)
1271
- if (!obj || typeof obj !== 'object') return null
1272
- const pick = (arr) => {
1273
- if (!Array.isArray(arr) || !arr.length) return null
1274
- const ids = arr.map((m) => (typeof m === 'string' ? m : m && m.id)).filter((s) => typeof s === 'string' && s.trim())
1275
- return ids.length ? [...new Set(ids)] : null
1276
- }
1277
- const agents = Array.isArray(obj.agents) ? obj.agents : []
1278
- const cli = agents.find((a) => a && a.name === 'cli')
1279
- const fromCli = cli ? pick(cli.models) : null
1280
- const fromTop = pick(obj.models)
1281
- if (fromCli && fromTop) return fromTop.length > fromCli.length ? fromTop : fromCli
1282
- return fromCli || fromTop || null
1283
- } catch (e) { return null }
1284
- }
1285
-
1286
- function parseCreditValue(raw) {
1287
- if (typeof raw !== 'string') return null
1288
- const s = raw.trim()
1289
- let m = s.match(/^[xX×]\s*([0-9]+(?:\.[0-9]+)?)/)
1290
- if (!m) m = s.match(/^([0-9]+(?:\.[0-9]+)?)\s*[xX×]/)
1291
- if (!m) return null
1292
- const n = Number(m[1])
1293
- return Number.isFinite(n) ? n : null
1294
- }
1295
-
1296
- function formatCreditLabel(credits) {
1297
- if (typeof credits !== 'number' || !Number.isFinite(credits)) return ''
1298
- if (credits === 0) return '免费'
1299
- return `x${credits.toFixed(2)}`
1300
- }
1301
-
1302
- function extractModelCredits(productJson) {
1303
- try {
1304
- const obj = normalizeProductConfig(productJson)
1305
- if (!obj || typeof obj !== 'object' || !Array.isArray(obj.models)) return null
1306
- const map = new Map()
1307
- for (const m of obj.models) {
1308
- if (!m || typeof m !== 'object') continue
1309
- const id = typeof m.id === 'string' ? m.id.trim() : ''
1310
- if (!id) continue
1311
- const n = parseCreditValue(m.credits)
1312
- if (n !== null) map.set(id, n)
1313
- }
1314
- return map.size ? map : null
1315
- } catch (e) { return null }
1316
- }
1317
-
1318
- function lookupModelCredits(creditsMap, id) {
1319
- if (!creditsMap || typeof id !== 'string') return null
1320
- const norm = id.trim()
1321
- const cands = [norm]
1322
- if (norm.endsWith('-ioa')) cands.push(norm.slice(0, -4))
1323
- else cands.push(norm + '-ioa')
1324
- if (CREDIT_ID_ALIASES[norm]) cands.push(...CREDIT_ID_ALIASES[norm])
1325
- for (const c of cands) {
1326
- if (creditsMap.has(c)) {
1327
- const v = creditsMap.get(c)
1328
- if (typeof v === 'number') return v
1329
- }
1330
- }
1331
- return null
1332
- }
1333
-
1334
- function backendModelCatalog(backend, io) {
1335
- const known = BACKEND_MODEL_IDS[backend] || []
1336
- const read = (io && io.readFileSync) || cbCatalogRead
1337
- let fromFile = null
1338
- let fromFileRich = null
1339
- // 旧 v1.6.1 语义:第一份有内容的 + 第一份带促销的,优先后者。
1340
- for (const p of productDescriptorCandidates(backend, io)) {
1341
- let txt = null
1342
- try { txt = read ? read(p, 'utf8') : null } catch (e) { continue }
1343
- if (txt == null) continue
1344
- const ids = extractModelIds(txt)
1345
- if (ids && ids.length) {
1346
- if (!fromFile) fromFile = ids
1347
- const obj = normalizeProductConfig(txt)
1348
- if (!fromFileRich && obj && Array.isArray(obj.modelPromotions)) fromFileRich = ids
1349
- }
1350
- if (fromFileRich) break
1351
- }
1352
- if (!fromFile) return known
1353
- return fromFileRich || fromFile
1354
- }
1355
-
1356
- // 服务端回吐的账号清单缓存(进程内,旧 core verbatim)。
1357
- const ACCOUNT_CATALOG_CACHE = new Map()
1358
- function rememberAccountCatalog(backend, ids) {
1359
- if (Array.isArray(ids) && ids.length) ACCOUNT_CATALOG_CACHE.set(backend, ids.slice())
1360
- }
1361
- function getAccountCatalog(backend) {
1362
- const v = ACCOUNT_CATALOG_CACHE.get(backend)
1363
- return v && v.length ? v.slice() : null
1364
- }
1365
-
1366
- // ── 实时模型清单(CLI 是一手权威来源,对齐 workbuddy-auth 的「权威白名单」思路)──
1367
- //
1368
- // 为什么不再以静态表/磁盘描述文件为主:实测本机 codebuddy CLI --
1369
- // • 账号实时清单(用无效模型触发 400 报文)= 19 项
1370
- // • CLI `--help` 声明的 roster = 19 项,与账号清单完全一致
1371
- // • 我们原先读的 product.ioa.json 静态表 = 16 项,且 2 个 id 已过时
1372
- // (minimax-m3 → minimax-m3-pay、kimi-k3-1 → kimi-k3-2),另缺 4 个新模型。
1373
- // 静态表是「随包冻结」的,必然落后于 CLI,故只保留作倍率元数据与最后兜底。
1374
- //
1375
- // 两级实时来源(都不静默冒充权威数据,各带 source 标签):
1376
- // A. account-live —— 起 CLI 用无效模型探测「该账号当前支持」的清单(需该面已登录,
1377
- // 一次网络往返;对照 workbuddy-auth 的 agents[cli].models 白名单语义)。
1378
- // B. cli-help —— 解析 `--help` 的 "Currently supported: (...)"(零网络、随 CLI
1379
- // 版本自动更新;产品面级 roster)。
1380
- // 静态表仅作 credit 元数据与 descriptor-static 兜底标签,绝不冒充实时。
1381
-
1382
- /** 解析 CLI `--help` 里声明的模型 roster。空/畸形输入返回 []。
1383
- * ⚠️ 名册很长时 yargs 会在任意列**折行**(实测把 `minimax-m3-pay` 从中间断成
1384
- * `minimax-` + `m3-pay`),若按行/按逗号直接切会丢项或产生残片(同一命令两次运行
1385
- * 给出 18/19 两种结果)。故先去掉括号内所有空白再按逗号切:逗号是唯一分隔符,
1386
- * 折行插入的空白一律视为噪声。 */
1387
- function parseHelpModels(helpText) {
1388
- const m = /Currently supported:\s*\(([^)]+)\)/.exec(String(helpText || ''))
1389
- if (!m) return []
1390
- const out = []
1391
- for (const raw of m[1].replace(/\s+/g, '').split(',')) {
1392
- const id = raw.trim()
1393
- if (id && /^[A-Za-z0-9_.\-]+$/.test(id) && out.indexOf(id) < 0) out.push(id)
1394
- }
1395
- return out
1396
- }
1397
-
1398
- /** 解析 codebuddy CLI 的可执行候选(env > npm 全局 shim > PATH 名)。 */
1399
- function cbCatalogCliCandidates(env) {
1400
- const e = env || cbCatalogEnv || {}
1401
- const out = []
1402
- const push = (p) => { if (p && out.indexOf(p) < 0) out.push(p) }
1403
- if (e.CODEBUDDY_BIN) push(String(e.CODEBUDDY_BIN))
1404
- const joinP = (a, b) => cbCatalogPathJoin(a, b)
1405
- if (e.APPDATA) push(joinP(joinP(e.APPDATA, 'npm'), 'codebuddy.cmd'))
1406
- if (e.APPDATA) push(joinP(joinP(e.APPDATA, 'npm'), 'codebuddy'))
1407
- push('codebuddy')
1408
- return out
1409
- }
1410
-
1411
- const CLI_MODEL_CACHE = new Map() // backend → { at, mode, ids }
1412
- const CLI_MODEL_TTL_MS = 5 * 60 * 1000
1413
-
1414
- // Windows .cmd/.bat 需要经 shell(Node 安全限制),但 `shell:true` + args 数组会触发
1415
- // DEP0190(参数不转义)。这里统一做法:把命令与参数拼成【一个】字符串交给 shell,
1416
- // 参数各自按 Windows 规则加引号 —— 既避开 DEP0190,又保证含空格参数不被拆开。
1417
- function quoteWinArg(a) {
1418
- const s = String(a)
1419
- if (s === '') return '""'
1420
- if (!/[\s"&|<>^]/.test(s)) return s
1421
- return '"' + s.replace(/"/g, '\\"') + '"'
1422
- }
1423
- function cliCommandLine(exe, args) {
1424
- return [exe].concat(args || []).map(quoteWinArg).join(' ')
1425
- }
1426
-
1427
- /** 把候选拆成 { cmd, prefixArgs }:.mjs/.js/.cjs 目标经 node 包装(测试夹具与自定义
1428
- * 入口都靠这条),其余原样。 */
1429
- function splitExeSpec(exe) {
1430
- if (/\.(mjs|cjs|js)$/i.test(String(exe))) {
1431
- const proc = globalThis.process
1432
- return { cmd: (proc && proc.execPath) ? proc.execPath : 'node', prefixArgs: [String(exe)] }
1433
- }
1434
- return { cmd: String(exe), prefixArgs: [] }
1435
- }
1436
-
1437
- function cliSpawnSync(exe, args, opts) {
1438
- const proc = globalThis.process
1439
- const child = (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule('node:child_process') : null
1440
- if (!child || typeof child.spawnSync !== 'function') return null
1441
- const base = Object.assign({
1442
- encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe']
1443
- }, opts || {})
1444
- const spec = splitExeSpec(exe)
1445
- const argv = spec.prefixArgs.concat(args || [])
1446
- const isCmd = /\.(cmd|bat)$/i.test(spec.cmd)
1447
- if (isCmd) base.shell = true
1448
- try {
1449
- // shell 形态不带 args 数组(否则 DEP0190);非 shell 形态正常传数组。
1450
- return isCmd
1451
- ? child.spawnSync(cliCommandLine(spec.cmd, argv), base)
1452
- : child.spawnSync(spec.cmd, argv, base)
1453
- } catch (e) { return null }
1454
- }
1455
-
1456
- /**
1457
- * 同步取「CLI 声明的 roster」(零网络,随 CLI 版本走)。
1458
- * @returns {{ok:boolean, ids:string[], mode:string, error?:string}}
1459
- */
1460
- function fetchHelpModelsSync(env, opts) {
1461
- const o = opts || {}
1462
- const cands = cbCatalogCliCandidates(env)
1463
- let lastErr = null
1464
- for (const exe of cands) {
1465
- const r = cliSpawnSync(exe, ['--help'], { timeout: Math.max(1000, Math.min(o.timeoutMs || 12000, 30000)) })
1466
- if (!r || r.error) { lastErr = 'spawn failed: ' + String((r && r.error && r.error.message) || 'unavailable'); continue }
1467
- const text = String(r.stdout || '') + String(r.stderr || '')
1468
- const ids = parseHelpModels(text)
1469
- if (ids.length) return { ok: true, ids, mode: 'cli-help', exe }
1470
- lastErr = 'no roster in --help output'
1471
- }
1472
- return { ok: false, ids: [], mode: 'cli-help', error: lastErr || 'codebuddy CLI not found (set CODEBUDDY_BIN)' }
1473
- }
1474
-
1475
- /**
1476
- * 同步版的账号探测(供设置面板用 —— 面板必须同步返回)。
1477
- * 走 spawnSync + 超时;结果进 5 分钟缓存,避免每次渲染都打一次网络。
1478
- *
1479
- * ⚠️ 为什么面板不能用 `--help` 名册顶替:实测本机 CLI 2.167.0 的 help 文本**本身
1480
- * 就是过时的** —— 它列 18 项且含已不存在的 `minimax-m3` / `kimi-k3-1`,缺真实的
1481
- * `glm-5.3-flashx` / `minimax-m3-pay` / `kimi-k3-2`;而账号探测给出 19 项且全部有效。
1482
- * 也就是说「CLI 声明」≠「服务端实际可用」,拿它当兜底会直接把错误清单展示给用户。
1483
- * 故这里宁可空列表 + 明确提示,也不展示已知错误的 roster。
1484
- * @returns {{ok:boolean, ids:string[], mode:string, error?:string}}
1485
- */
1486
- function probeAccountModelsSync(env, opts) {
1487
- const o = opts || {}
1488
- const cands = cbCatalogCliCandidates(env)
1489
- const extraEnv = o.envExtra || {}
1490
- const reasons = []
1491
- const timeout = Math.max(3000, Math.min(o.timeoutMs || 12000, 30000))
1492
- for (const exe of cands) {
1493
- const r = cliSpawnSync(exe, ['-p', 'hi', '--model', '__clb_probe_invalid__', '--output-format', 'text'], {
1494
- timeout,
1495
- env: Object.assign({}, (globalThis.process && globalThis.process.env) || {}, extraEnv)
1496
- })
1497
- if (!r || r.error) { reasons.push(exe + ': ' + String((r && r.error && (r.error.code || r.error.message)) || 'spawn failed')); continue }
1498
- const out = String(r.stdout || '') + String(r.stderr || '')
1499
- const ids = parseAccountModels(out)
1500
- if (ids && ids.length) return { ok: true, ids, mode: 'account-live', exe }
1501
- const head = String(out).replace(/\s+/g, ' ').trim().slice(0, 140)
1502
- reasons.push(exe + ': exit ' + r.status + ', no account roster' + (head ? ' — ' + head : ''))
1503
- }
1504
- return { ok: false, ids: [], mode: 'account-live', error: reasons.join('; ') || 'no CLI candidate to probe' }
1505
- }
1506
-
1507
- /**
1508
- * 异步探测「该账号当前支持」的清单:用无效模型触发 CLI 的 400 报文。
1509
- * 对齐 workbuddy-auth 的权威白名单语义(服务器告诉我们这个账号能用什么)。
1510
- * @returns {Promise<{ok:boolean, ids:string[], mode:string, error?:string}>}
1511
- */
1512
- function probeAccountModels(env, opts) {
1513
- const o = opts || {}
1514
- const cands = cbCatalogCliCandidates(env)
1515
- const proc = globalThis.process
1516
- const child = (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule('node:child_process') : null
1517
- if (!child || typeof child.spawn !== 'function') {
1518
- return Promise.resolve({ ok: false, ids: [], mode: 'account-live', error: 'no spawn capability in this host form' })
1519
- }
1520
- const extraEnv = o.envExtra || {}
1521
- return new Promise((resolve) => {
1522
- let idx = 0
1523
- // 记录每个候选的真实结局:区分「压根没找到 CLI」与「CLI 跑了但没吐账号名录」
1524
- // (后者通常是该面未登录 / 端点拒了)—— 报错文案必须能指导下一步,不能一律
1525
- // 说 "CLI not found"(本轮就踩到:codebuddy-en 无 token 时被误报成找不到 CLI)。
1526
- const reasons = []
1527
- const attempt = () => {
1528
- if (idx >= cands.length) {
1529
- resolve({
1530
- ok: false, ids: [], mode: 'account-live',
1531
- error: reasons.length ? reasons.join('; ') : 'no CLI candidate to probe',
1532
- notFound: reasons.every((r) => /not found|ENOENT|spawn/i.test(r))
1533
- })
1534
- return
1535
- }
1536
- const exe = cands[idx++]
1537
- let c
1538
- try {
1539
- // .cmd/.bat 需经 shell(Windows 安全限制);.mjs/.js 经 node 包装。
1540
- // 与 cliSpawnSync 同一规则:shell 形态把命令拼成单字符串(避免 DEP0190)。
1541
- const spec = splitExeSpec(exe)
1542
- const isCmd = /\.(cmd|bat)$/i.test(spec.cmd)
1543
- const probeArgs = spec.prefixArgs.concat(['-p', 'hi', '--model', '__clb_probe_invalid__', '--output-format', 'text'])
1544
- const opts = {
1545
- stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
1546
- env: Object.assign({}, proc.env, extraEnv)
1547
- }
1548
- c = isCmd
1549
- ? child.spawn(cliCommandLine(spec.cmd, probeArgs), Object.assign({ shell: true }, opts))
1550
- : child.spawn(spec.cmd, probeArgs, opts)
1551
- } catch (e) { reasons.push(exe + ': spawn threw ' + String(e && e.message || e)); attempt(); return }
1552
- let out = ''
1553
- const timer = setTimeout(() => { try { c.kill() } catch (e) { } }, Math.max(5000, Math.min(o.timeoutMs || 30000, 90000)))
1554
- const onData = (d) => { out += d; if (out.length > 400000) out = out.slice(-200000) }
1555
- c.stdout.on('data', onData)
1556
- c.stderr.on('data', onData)
1557
- c.on('error', (e) => {
1558
- clearTimeout(timer)
1559
- reasons.push(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
1560
- attempt()
1561
- })
1562
- c.on('close', (code) => {
1563
- clearTimeout(timer)
1564
- const ids = parseAccountModels(out)
1565
- if (ids && ids.length) { resolve({ ok: true, ids, mode: 'account-live', exe }); return }
1566
- const head = String(out).replace(/\s+/g, ' ').trim().slice(0, 160)
1567
- reasons.push(exe + ': exit ' + code + ', no account roster' + (head ? ' — ' + head : ''))
1568
- attempt()
1569
- })
1570
- }
1571
- attempt()
1572
- })
1573
- }
1574
-
1575
- /** 缓存读写(面板每次渲染都会取,避免反复起进程)。 */
1576
- function cachedCliModels(backend, load, nowMs) {
1577
- const at = Number(nowMs) || Date.now()
1578
- const hit = CLI_MODEL_CACHE.get(backend)
1579
- if (hit && at - hit.at < CLI_MODEL_TTL_MS) return hit
1580
- const v = typeof load === 'function' ? load() : load
1581
- if (v && v.ok && v.ids && v.ids.length) CLI_MODEL_CACHE.set(backend, { at, mode: v.mode, ids: v.ids.slice() })
1582
- return v
1583
- }
1584
-
1585
- /** 读统一设置文件里 codebuddy 分区的 preferredBackend(meta 需要它才知道列哪个面)。 */
1586
- function currentPreferredBackend() {
1587
- try {
1588
- const e = cbCatalogEnv || {}
1589
- const p = cbCatalogRead(e.CLI_BRIDGE_SETTINGS_FILE || cbCatalogPathJoin(
1590
- e.DSH_HOME || (e.APPDATA ? cbCatalogPathJoin(e.APPDATA, 'DSH Desktop', 'dsh-home') : ''), 'cli-bridge-settings.json'))
1591
- if (!p) return null
1592
- const all = JSON.parse(p)
1593
- const v = all && all.codebuddy && all.codebuddy.preferredBackend
1594
- return typeof v === 'string' && v ? v : null
1595
- } catch (err) { return null }
1596
- }
1597
-
1598
- // CLI 报错里解析「该账号当前支持的模型」(旧 core parseAccountModels verbatim)。
1599
- function parseAccountModels(text) {
1600
- const s = String(text || '')
1601
- const anchor = s.indexOf('Currently supported models for your account')
1602
- if (anchor < 0) return null
1603
- const tail = s.slice(anchor)
1604
- const ids = []
1605
- for (const line of tail.split(/\r?\n/)) {
1606
- const m = /^\s*[-*]\s+(\S+)\s*$/.exec(line)
1607
- if (!m) {
1608
- if (ids.length) break
1609
- continue
1610
- }
1611
- if (!ids.includes(m[1])) ids.push(m[1])
1612
- }
1613
- return ids.length ? ids : null
1614
- }
1615
-
1616
- // 带倍率/授权的详细清单(无促销角标,旧 v1.6.1 的 0.1.0 子集;排序规则 verbatim:
1617
- // 已授权在前 → 有倍率升序 → 无倍率殿后)。
1618
- function modelCatalogDetailed(backend, io) {
1619
- const o = io || {}
1620
- const fromFile = backendModelCatalog(backend, o)
1621
- const acct = (Array.isArray(o.accountModels) && o.accountModels.length) ? o.accountModels : getAccountCatalog(backend)
1622
- const ids = []
1623
- const authorized = new Set()
1624
- if (acct) for (const id of acct) { if (!ids.includes(id)) ids.push(id); authorized.add(id) }
1625
- for (const id of fromFile) if (!ids.includes(id)) ids.push(id)
1626
- const read = o.readFileSync || cbCatalogRead
1627
- let creditsMap = null
1628
- for (const p of productDescriptorCandidates(backend, io)) {
1629
- let txt = null
1630
- try { txt = read ? read(p, 'utf8') : null } catch (e) { continue }
1631
- if (txt == null) continue
1632
- const map = extractModelCredits(txt)
1633
- if (map) { creditsMap = map; break }
1634
- }
1635
- const items = ids.map((id) => {
1636
- const base = lookupModelCredits(creditsMap, id)
1637
- const credits = base
1638
- const hasCredits = typeof credits === 'number'
1639
- const suffix = formatCreditLabel(credits)
1640
- const marks = []
1641
- if (suffix) marks.push(suffix)
1642
- if (acct && !authorized.has(id)) marks.push('未授权')
1643
- return {
1644
- id,
1645
- credits,
1646
- baseCredits: base,
1647
- free: hasCredits && credits === 0,
1648
- hasCredits,
1649
- authorized: acct ? authorized.has(id) : null,
1650
- promotion: null,
1651
- label: marks.length ? `${id} · ${marks.join(' · ')}` : id
1652
- }
1653
- })
1654
- const rank = (x) => (x.authorized === false ? 1 : 0)
1655
- const withC = items.filter((x) => x.hasCredits).sort((a, b) => rank(a) - rank(b) || a.credits - b.credits)
1656
- const withoutC = items.filter((x) => !x.hasCredits).sort((a, b) => rank(a) - rank(b))
1657
- return [...withC, ...withoutC]
1658
- }
1659
-
1660
- function formatModelCatalogText(backend, io) {
1661
- return modelCatalogDetailed(backend, io).map((x) => x.label).join(', ')
1662
- }
1663
-
1664
-
1665
- const BACKEND_AUTH_IDS = {
1666
- 'codebuddy': 'Tencent-Cloud.coding-copilot',
1667
- 'codebuddy-intl': 'Tencent-Cloud.coding-copilot',
1668
- 'codebuddy-en': 'workbuddy-desktop-ai',
1669
- 'workbuddy': 'workbuddy-desktop'
1670
- }
1671
-
1672
- const CREDENTIAL_KEYS = ['WORKBUDDY_TOKEN', 'WORKBUDDY_AI_TOKEN', 'CODEBUDDY_EN_TOKEN']
1673
-
1674
- // ── 凭据读取(搬自 codebuddy.mjs;fs/path 走 cbCatalog* 守卫,env 经 io 或注入)
1675
- function readBackendAuthDomain(backend, io) {
1676
- const o = io || {}
1677
- const id = BACKEND_AUTH_IDS[backend]
1678
- if (!id) return null
1679
- try {
1680
- const read = o.readFileSync || cbCatalogRead
1681
- if (typeof read !== 'function') return null
1682
- const e = o.env || env
1683
- let dir = o.dir
1684
- if (!dir) {
1685
- dir = e.CODEBUDDY_AUTH_DIR ||
1686
- cbCatalogPathJoin(e.LOCALAPPDATA || 'C:\\Users\\lcl\\AppData\\Local', 'CodeBuddyExtension', 'Data', 'Public', 'auth')
1687
- }
1688
- const sep = String(dir).indexOf('\\') >= 0 ? '\\' : '/'
1689
- const txt = read(dir + sep + id + '.info', 'utf8')
1690
- if (txt == null) return null
1691
- const raw = JSON.parse(txt)
1692
- const d = raw && raw.auth && raw.auth.domain
1693
- return typeof d === 'string' && d.trim() ? d.trim() : null
1694
- } catch (e) { return null }
1695
- }
1696
-
1697
- // DSH 凭据库第三通道(旧 core readDshWorkbuddyToken verbatim 语义;CODEBUDDY_
1698
- // CREDENTIALS_DIR 覆盖测试夹具;fs 经 deps 注入)。
1699
- function readDshWorkbuddyToken(io) {
1700
- const e = (io && io.env) || cbCatalogEnv
1701
- const read = (io && io.readFileSync) || cbCatalogRead
1702
- const overrideDir = e.CODEBUDDY_CREDENTIALS_DIR || null
1703
- const home = overrideDir || e.DSH_HOME || null
1704
- if (!home) return null
1705
- const files = [home + '\\.credentials.yaml', home + '\\.env', home + '/.credentials.yaml', home + '/.env']
1706
- for (const f of files) {
1707
- const text = read(f)
1708
- if (!text) continue
1709
- for (const key of CREDENTIAL_KEYS) {
1710
- const m = text.match(new RegExp('^[ \\t]*' + key + '[ \\t]*[:=][ \\t]*(.+)$', 'm'))
1711
- if (!m) continue
1712
- const v = m[1].trim().replace(/\s+#.*$/, '').replace(/^['"]|['"]$/g, '').trim()
1713
- // 只接受看起来是真实 token 的值(避免把 $VAR 引用或占位符当凭据)。
1714
- if (v.length >= 40 && !v.startsWith('$') && !/^<.*>$/.test(v)) return v
1715
- }
1716
- }
1717
- return null
1718
- }
1719
-
1720
- // codebuddy-en 凭据链(旧 core verbatim):设置面板 > CODEBUDDY_AUTH_TOKEN >
1721
- // DSH 凭据库。dynamic 沙箱无 fs → 第三通道自然缺席(旧 dynamic 本也有限)。
1722
- function resolveEnToken(settingToken, io) {
1723
- const e = (io && io.env) || cbCatalogEnv
1724
- const s = String(settingToken || '').trim()
1725
- if (s) return { token: s, source: 'setting' }
1726
- const envTok = e.CODEBUDDY_AUTH_TOKEN || ''
1727
- if (String(envTok).trim()) return { token: String(envTok).trim(), source: 'env' }
1728
- const fromStore = readDshWorkbuddyToken(io)
1729
- if (fromStore) return { token: fromStore, source: 'dsh-store' }
1730
- return { token: null, source: null }
1731
- }
1732
-
1159
+ // providers/codebuddy-catalog.mjs — codebuddy 模型目录/积分/描述文件解析(纯逻辑)。
1160
+ //
1161
+ // 从 providers/codebuddy.mjs 拆出:主文件原 864 行远超 600 行红线(规则 §26)。
1162
+ // 只放「模型目录相关」纯逻辑:静态表、描述文件候选路径、模型 id 抽取、积分倍率、
1163
+ // 账号清单收割、目录渲染。不含 CLI 调用、状态机、凭据链。
1164
+ //
1165
+ // 沙箱纪律(与 providers/*.mjs 同):本文件会被 scripts/build.mjs 经模板顶部的
1166
+ // 第三个占位标记内联进同一个 dynamic 沙箱脚本作用域 —— 所以:
1167
+ // 1) 不得 import 任何模块(import 行会被 stripModuleSyntax 剥掉,函数体留下但
1168
+ // 依赖丢失 → 运行期 ReferenceError);node 内建走 cbCatalog* 守卫;
1169
+ // 2) 顶层标识符不得与 bridge-kit / codebuddy.mjs 剩余部分撞名(同一作用域重复
1170
+ // 声明 = 构建期 Identifier already declared);
1171
+ // 3) 不得出现 import.meta 字面量;
1172
+ // 4) ⚠️ 注释里绝不写出那个占位标记的字面量:build 做全局文本替换,注释中出现
1173
+ // 同样会被替换成正文代码,把注释从中间劈开 → 生成物语法崩。
1174
+
1175
+ // ── 宿主能力注入(env / fs / path / sep)───────────────────────────────────
1176
+ // 本模块函数搬自主文件的闭包,拿不到那边的 env / fsRead / pathJoin / pathSep。
1177
+ // 由 codebuddy.mjs 在 defineProvider 里调 cbCatalogSetHost() 注入;未注入时退到
1178
+ // getBuiltinModule 守卫(沙箱里没有 → 返回 null / 默认分隔符,绝不抛)。
1179
+ // 名字一律带 cbCatalog_ 前缀:内联后与主文件的 fsRead/pathJoin 同处一个作用域。
1180
+ // export 供 codebuddy-models-source.mjs 读取:ESM live binding,cbCatalogSetHost 后自动同步。
1181
+ let cbCatalogEnv = {}
1182
+ let cbCatalogFs = null
1183
+ let cbCatalogPathSep = "/"
1184
+
1185
+ function cbCatalogSetHost(envObj, pathMod, fsMod) {
1186
+ if (envObj && typeof envObj === "object") cbCatalogEnv = envObj
1187
+ if (fsMod && typeof fsMod.readFileSync === "function") cbCatalogFs = fsMod
1188
+ if (pathMod && typeof pathMod.sep === "string") cbCatalogPathSep = pathMod.sep
1189
+ else if (/[A-Za-z]:[\\/]/.test(String((cbCatalogEnv && (cbCatalogEnv.USERPROFILE || cbCatalogEnv.APPDATA)) || ""))) cbCatalogPathSep = "\\"
1190
+ }
1191
+
1192
+ function cbCatalogRead(p, enc) {
1193
+ if (!p) return null
1194
+ try {
1195
+ if (cbCatalogFs && typeof cbCatalogFs.readFileSync === "function") return cbCatalogFs.readFileSync(p, enc || "utf8")
1196
+ const proc = globalThis.process
1197
+ const nf = (proc && typeof proc.getBuiltinModule === "function") ? proc.getBuiltinModule("node:fs") : null
1198
+ if (!nf || typeof nf.readFileSync !== "function") return null
1199
+ return nf.readFileSync(p, enc || "utf8")
1200
+ } catch (e) { return null }
1201
+ }
1202
+
1203
+ function cbCatalogPathJoin(...parts) {
1204
+ try {
1205
+ const proc = globalThis.process
1206
+ const np = (proc && typeof proc.getBuiltinModule === "function") ? proc.getBuiltinModule("node:path") : null
1207
+ if (np && typeof np.join === "function") return np.join(...parts)
1208
+ } catch (e) { }
1209
+ return parts.join("/")
1210
+ }
1211
+
1212
+ // ── 以下常量与函数原样搬自 providers/codebuddy.mjs(行为 zero-change)────────
1213
+
1214
+ const BACKEND_MODEL_IDS = {
1215
+ 'codebuddy': ['hy4-preview', 'hy3', 'hy3-x', 'deepseek-v4.1-flash', 'glm-5.3', 'glm-5.3-flash', 'glm-5.2', 'glm-5.1', 'glm-5v-turbo', 'minimax-m3', 'minimax-m2.7', 'kimi-k3-1', 'kimi-k2.8-preview', 'kimi-k2.7', 'kimi-k2.6', 'deepseek-v4-pro'],
1216
+ 'codebuddy-intl': ['claude-sonnet-5', 'claude-sonnet-5-1m', 'claude-opus-5', 'claude-opus-4.8', 'claude-opus-4.8-1m', 'gemini-3.1-pro', 'gemini-3.5-flash', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.5', 'gpt-5.4', 'glm-5.3-ioa', 'glm-5.3-flash-ioa', 'glm-5.2-ioa', 'kimi-k3-ioa', 'kimi-k2.8-preview', 'kimi-k2.6-ioa', 'minimax-m3-ioa', 'minimax-m2.7-ioa', 'deepseek-v4.1-flash', 'deepseek-v4-pro-ioa'],
1217
+ // WorkBuddyAI 自制目录(product.json 顶层 37 项,按其 credits 排序:免费的在前)
1218
+ 'codebuddy-en': ['hy3', 'gpt-5.1-codex-mini', 'gemini-3.1-flash-lite', 'gemini-2.5-flash', 'minimax-m3', 'gemini-3.0-flash', 'fast-model', 'deepseek-v3-2-volc', 'balanced-model', 'kimi-k2.5', 'kimi-k2.6', 'default-model-lite', 'glm-5.3', 'glm-5.2', 'glm-5.0', 'gpt-5.1-codex', 'gemini-2.5-pro', 'gemini-3.5-flash', 'gpt-5.3-codex', 'gemini-3.1-pro', 'gpt-5.6-terra', 'kimi-k3', 'gpt-5.4', 'primary-model', 'default-model', 'deep-model', 'gpt-5.5', 'gpt-5.6-sol', 'gpt-5.6-luna'],
1219
+ // WorkBuddy 国内桌面自制目录(product.json:applicationName=WorkBuddy)
1220
+ 'workbuddy': ['auto', 'hy4-preview', 'hy3', 'hy3-x', 'deepseek-v4.1-flash', 'glm-5.3', 'glm-5.3-flash', 'glm-5.2', 'glm-5.1', 'glm-5v-turbo', 'minimax-m3', 'minimax-m2.7', 'kimi-k3-1', 'kimi-k2.8-preview', 'kimi-k2.7', 'kimi-k2.6', 'deepseek-v4-pro']
1221
+ }
1222
+
1223
+ // 各后端真正生效的 product 描述文件位置(旧 core v1.6.1 verbatim)。
1224
+ const PRODUCT_DESCRIPTOR_ENV = {
1225
+ 'codebuddy': { env: 'CODEBUDDY_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: null },
1226
+ 'codebuddy-intl': { env: 'CODEBUDDY_INTL_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: null },
1227
+ 'codebuddy-en': { env: 'CODEBUDDY_EN_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: '.workbuddy-ai' },
1228
+ 'workbuddy': { env: 'WORKBUDDY_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: '.workbuddy' }
1229
+ }
1230
+
1231
+ // 倍率跨命名匹配(账号清单 id → 描述文件 id,旧 core verbatim)。
1232
+ const CREDIT_ID_ALIASES = {
1233
+ 'kimi-k3-1': ['kimi-k3', 'kimi-k3-ioa'],
1234
+ 'hy3-x': ['hy3', 'hy3-ioa'],
1235
+ 'default': ['codewise-default-model-v2'],
1236
+ 'auto': ['hy3', 'hy3-ioa']
1237
+ }
1238
+
1239
+ const MODEL_DESCRIPTION = 'Optional model id. When unspecified, the user-preferred default model (plugin settings) is used if set, else the CLI default. Two layers decide: the PRODUCT FACE decides whether the id exists in its catalogue, then the ACCOUNT decides whether it is licensed. The free tier (hy3, deepseek-v4.1-flash at x0.00 credits; hy4-preview at x0.29) belongs to the npm CLI faces codebuddy/codebuddy-intl: hy4-preview, hy3, hy3-x, deepseek-v4.1-flash, glm-5.3, glm-5.3-flash, glm-5.2, glm-5.1, glm-5v-turbo, minimax-m3, minimax-m2.7, kimi-k3-1, kimi-k2.8-preview, kimi-k2.7, kimi-k2.6, deepseek-v4-pro. codebuddy-en (WorkBuddyAI) carries NO hy4-preview and NO deepseek-v4.1-flash; its only free model is hy3. International ids (claude-sonnet-5, gemini-3.1-pro, ...) are account-gated. Availability is per ACCOUNT: the CLI rejects an id outside the account catalogue with "400 service info not found" and one inside it but unlicensed with "400 only available for authorized users" (many catalogue flagships such as claude-opus-4.8, gpt-5.4 and gpt-6-astra are in this second group for a domestic account). Run the CLI with a deliberately invalid model id to print "Currently supported models for your account:" and get the authoritative list; the plugin caches that result and the settings panel lists exactly those ids with their credit multiplier (x0.00 = free), read live from the CLI descriptor so it stays in sync with CLI upgrades. Prefer the cheapest model that can do the job, and prefer a free (x0.00) one for routine work. Pass a model only when the task clearly benefits from a specific one.'
1240
+
1241
+ const BACKEND_ENUM = ['codebuddy', 'codebuddy-intl', 'codebuddy-ioa', 'codebuddy-international', 'codebuddy-en', 'workbuddy-en', 'workbuddy-ai', 'workbuddy']
1242
+
1243
+ function accountConfigCandidates(backend, io) {
1244
+ const e = (io && io.env) || cbCatalogEnv
1245
+ const spec = PRODUCT_DESCRIPTOR_ENV[backend]
1246
+ if (!spec || !spec.accountDir) return []
1247
+ const home = e.USERPROFILE || e.HOME
1248
+ if (!home) return []
1249
+ const base = cbCatalogPathJoin(home, spec.accountDir.split('/').join(cbCatalogPathSep))
1250
+ return [
1251
+ cbCatalogPathJoin(base, 'cache', 'acc-product-config-v3.json'),
1252
+ cbCatalogPathJoin(base, 'local_storage', 'entry_d43e96994f944cfb77961c2ea7d04605.info'),
1253
+ cbCatalogPathJoin(base, 'local_storage', 'wb_entry_d43e96994f944cfb77961c2ea7d04605.info')
1254
+ ]
1255
+ }
1256
+
1257
+ function productDescriptorCandidates(backend, io) {
1258
+ const e = (io && io.env) || cbCatalogEnv
1259
+ const spec = PRODUCT_DESCRIPTOR_ENV[backend]
1260
+ if (!spec) return []
1261
+ const out = []
1262
+ for (const p of accountConfigCandidates(backend, io)) out.push(p)
1263
+ if (e[spec.env]) out.push(e[spec.env])
1264
+ const rels = spec.fallbackRel ? [spec.rel, spec.fallbackRel] : [spec.rel]
1265
+ const push = (root) => { if (root) for (const r of rels) out.push(cbCatalogPathJoin(root, ...r.split('/'))) }
1266
+ if (backend === 'codebuddy' || backend === 'codebuddy-intl') {
1267
+ push(e.APPDATA ? cbCatalogPathJoin(e.APPDATA, 'npm', 'node_modules', '@tencent-ai', 'codebuddy-code') : null)
1268
+ } else if (backend === 'codebuddy-en') {
1269
+ push('C:\\Program Files\\WorkBuddyAI\\resources\\app.asar.unpacked\\cli')
1270
+ } else if (backend === 'workbuddy') {
1271
+ push('C:\\Program Files\\WorkBuddy\\resources\\app.asar.unpacked\\cli')
1272
+ }
1273
+ return out
1274
+ }
1275
+
1276
+ function normalizeProductConfig(raw) {
1277
+ try {
1278
+ const obj = typeof raw === 'string' ? JSON.parse(raw) : raw
1279
+ if (!obj) return null
1280
+ if (Array.isArray(obj)) {
1281
+ // 数组形态:从后往前找第一个带 data 的条目(末尾最新)。
1282
+ for (let i = obj.length - 1; i >= 0; i--) {
1283
+ const e = obj[i]
1284
+ if (e && typeof e === 'object' && e.data && typeof e.data === 'object') return e.data
1285
+ }
1286
+ return null
1287
+ }
1288
+ if (typeof obj !== 'object') return null
1289
+ if (obj.data && typeof obj.data === 'object' && !Array.isArray(obj.models) && !Array.isArray(obj.agents)) return obj.data
1290
+ return obj
1291
+ } catch (e) { return null }
1292
+ }
1293
+
1294
+ function extractModelIds(productJson) {
1295
+ try {
1296
+ const obj = normalizeProductConfig(productJson)
1297
+ if (!obj || typeof obj !== 'object') return null
1298
+ const pick = (arr) => {
1299
+ if (!Array.isArray(arr) || !arr.length) return null
1300
+ const ids = arr.map((m) => (typeof m === 'string' ? m : m && m.id)).filter((s) => typeof s === 'string' && s.trim())
1301
+ return ids.length ? [...new Set(ids)] : null
1302
+ }
1303
+ const agents = Array.isArray(obj.agents) ? obj.agents : []
1304
+ const cli = agents.find((a) => a && a.name === 'cli')
1305
+ const fromCli = cli ? pick(cli.models) : null
1306
+ const fromTop = pick(obj.models)
1307
+ if (fromCli && fromTop) return fromTop.length > fromCli.length ? fromTop : fromCli
1308
+ return fromCli || fromTop || null
1309
+ } catch (e) { return null }
1310
+ }
1311
+
1312
+ function parseCreditValue(raw) {
1313
+ if (typeof raw !== 'string') return null
1314
+ const s = raw.trim()
1315
+ let m = s.match(/^[xX×]\s*([0-9]+(?:\.[0-9]+)?)/)
1316
+ if (!m) m = s.match(/^([0-9]+(?:\.[0-9]+)?)\s*[xX×]/)
1317
+ if (!m) return null
1318
+ const n = Number(m[1])
1319
+ return Number.isFinite(n) ? n : null
1320
+ }
1321
+
1322
+ function formatCreditLabel(credits) {
1323
+ if (typeof credits !== 'number' || !Number.isFinite(credits)) return ''
1324
+ if (credits === 0) return '免费'
1325
+ return `x${credits.toFixed(2)}`
1326
+ }
1327
+
1328
+ function extractModelCredits(productJson) {
1329
+ try {
1330
+ const obj = normalizeProductConfig(productJson)
1331
+ if (!obj || typeof obj !== 'object' || !Array.isArray(obj.models)) return null
1332
+ const map = new Map()
1333
+ for (const m of obj.models) {
1334
+ if (!m || typeof m !== 'object') continue
1335
+ const id = typeof m.id === 'string' ? m.id.trim() : ''
1336
+ if (!id) continue
1337
+ const n = parseCreditValue(m.credits)
1338
+ if (n !== null) map.set(id, n)
1339
+ }
1340
+ return map.size ? map : null
1341
+ } catch (e) { return null }
1342
+ }
1343
+
1344
+ function lookupModelCredits(creditsMap, id) {
1345
+ if (!creditsMap || typeof id !== 'string') return null
1346
+ const norm = id.trim()
1347
+ const cands = [norm]
1348
+ if (norm.endsWith('-ioa')) cands.push(norm.slice(0, -4))
1349
+ else cands.push(norm + '-ioa')
1350
+ if (CREDIT_ID_ALIASES[norm]) cands.push(...CREDIT_ID_ALIASES[norm])
1351
+ for (const c of cands) {
1352
+ if (creditsMap.has(c)) {
1353
+ const v = creditsMap.get(c)
1354
+ if (typeof v === 'number') return v
1355
+ }
1356
+ }
1357
+ return null
1358
+ }
1359
+
1360
+ function backendModelCatalog(backend, io) {
1361
+ const known = BACKEND_MODEL_IDS[backend] || []
1362
+ const read = (io && io.readFileSync) || cbCatalogRead
1363
+ let fromFile = null
1364
+ let fromFileRich = null
1365
+ // 旧 v1.6.1 语义:第一份有内容的 + 第一份带促销的,优先后者。
1366
+ for (const p of productDescriptorCandidates(backend, io)) {
1367
+ let txt = null
1368
+ try { txt = read ? read(p, 'utf8') : null } catch (e) { continue }
1369
+ if (txt == null) continue
1370
+ const ids = extractModelIds(txt)
1371
+ if (ids && ids.length) {
1372
+ if (!fromFile) fromFile = ids
1373
+ const obj = normalizeProductConfig(txt)
1374
+ if (!fromFileRich && obj && Array.isArray(obj.modelPromotions)) fromFileRich = ids
1375
+ }
1376
+ if (fromFileRich) break
1377
+ }
1378
+ if (!fromFile) return known
1379
+ return fromFileRich || fromFile
1380
+ }
1381
+
1382
+ // 服务端回吐的账号清单缓存(进程内,旧 core verbatim)。
1383
+ const ACCOUNT_CATALOG_CACHE = new Map()
1384
+ function rememberAccountCatalog(backend, ids) {
1385
+ if (Array.isArray(ids) && ids.length) ACCOUNT_CATALOG_CACHE.set(backend, ids.slice())
1386
+ }
1387
+ function getAccountCatalog(backend) {
1388
+ const v = ACCOUNT_CATALOG_CACHE.get(backend)
1389
+ return v && v.length ? v.slice() : null
1390
+ }
1391
+
1392
+ function parseAccountModels(text) {
1393
+ const s = String(text || '')
1394
+ const anchor = s.indexOf('Currently supported models for your account')
1395
+ if (anchor < 0) return null
1396
+ const tail = s.slice(anchor)
1397
+ const ids = []
1398
+ for (const line of tail.split(/\r?\n/)) {
1399
+ const m = /^\s*[-*]\s+(\S+)\s*$/.exec(line)
1400
+ if (!m) {
1401
+ if (ids.length) break
1402
+ continue
1403
+ }
1404
+ if (!ids.includes(m[1])) ids.push(m[1])
1405
+ }
1406
+ return ids.length ? ids : null
1407
+ }
1408
+
1409
+ // 带倍率/授权的详细清单(无促销角标,旧 v1.6.1 的 0.1.0 子集;排序规则 verbatim:
1410
+ // 已授权在前 → 有倍率升序 → 无倍率殿后)。
1411
+ function modelCatalogDetailed(backend, io) {
1412
+ const o = io || {}
1413
+ const fromFile = backendModelCatalog(backend, o)
1414
+ const acct = (Array.isArray(o.accountModels) && o.accountModels.length) ? o.accountModels : getAccountCatalog(backend)
1415
+ const ids = []
1416
+ const authorized = new Set()
1417
+ if (acct) for (const id of acct) { if (!ids.includes(id)) ids.push(id); authorized.add(id) }
1418
+ for (const id of fromFile) if (!ids.includes(id)) ids.push(id)
1419
+ const read = o.readFileSync || cbCatalogRead
1420
+ let creditsMap = null
1421
+ for (const p of productDescriptorCandidates(backend, io)) {
1422
+ let txt = null
1423
+ try { txt = read ? read(p, 'utf8') : null } catch (e) { continue }
1424
+ if (txt == null) continue
1425
+ const map = extractModelCredits(txt)
1426
+ if (map) { creditsMap = map; break }
1427
+ }
1428
+ const items = ids.map((id) => {
1429
+ const base = lookupModelCredits(creditsMap, id)
1430
+ const credits = base
1431
+ const hasCredits = typeof credits === 'number'
1432
+ const suffix = formatCreditLabel(credits)
1433
+ const marks = []
1434
+ if (suffix) marks.push(suffix)
1435
+ if (acct && !authorized.has(id)) marks.push('未授权')
1436
+ return {
1437
+ id,
1438
+ credits,
1439
+ baseCredits: base,
1440
+ free: hasCredits && credits === 0,
1441
+ hasCredits,
1442
+ authorized: acct ? authorized.has(id) : null,
1443
+ promotion: null,
1444
+ label: marks.length ? `${id} · ${marks.join(' · ')}` : id
1445
+ }
1446
+ })
1447
+ const rank = (x) => (x.authorized === false ? 1 : 0)
1448
+ const withC = items.filter((x) => x.hasCredits).sort((a, b) => rank(a) - rank(b) || a.credits - b.credits)
1449
+ const withoutC = items.filter((x) => !x.hasCredits).sort((a, b) => rank(a) - rank(b))
1450
+ return [...withC, ...withoutC]
1451
+ }
1452
+
1453
+ function formatModelCatalogText(backend, io) {
1454
+ return modelCatalogDetailed(backend, io).map((x) => x.label).join(', ')
1455
+ }
1456
+
1457
+
1458
+ const BACKEND_AUTH_IDS = {
1459
+ 'codebuddy': 'Tencent-Cloud.coding-copilot',
1460
+ 'codebuddy-intl': 'Tencent-Cloud.coding-copilot',
1461
+ 'codebuddy-en': 'workbuddy-desktop-ai',
1462
+ 'workbuddy': 'workbuddy-desktop'
1463
+ }
1464
+
1465
+ const CREDENTIAL_KEYS = ['WORKBUDDY_TOKEN', 'WORKBUDDY_AI_TOKEN', 'CODEBUDDY_EN_TOKEN']
1466
+
1467
+ // ── 凭据读取(搬自 codebuddy.mjs;fs/path 走 cbCatalog* 守卫,env 经 io 或注入)
1468
+ function readBackendAuthDomain(backend, io) {
1469
+ const o = io || {}
1470
+ const id = BACKEND_AUTH_IDS[backend]
1471
+ if (!id) return null
1472
+ try {
1473
+ const read = o.readFileSync || cbCatalogRead
1474
+ if (typeof read !== 'function') return null
1475
+ const e = o.env || env
1476
+ let dir = o.dir
1477
+ if (!dir) {
1478
+ dir = e.CODEBUDDY_AUTH_DIR ||
1479
+ cbCatalogPathJoin(e.LOCALAPPDATA || 'C:\\Users\\lcl\\AppData\\Local', 'CodeBuddyExtension', 'Data', 'Public', 'auth')
1480
+ }
1481
+ const sep = String(dir).indexOf('\\') >= 0 ? '\\' : '/'
1482
+ const txt = read(dir + sep + id + '.info', 'utf8')
1483
+ if (txt == null) return null
1484
+ const raw = JSON.parse(txt)
1485
+ const d = raw && raw.auth && raw.auth.domain
1486
+ return typeof d === 'string' && d.trim() ? d.trim() : null
1487
+ } catch (e) { return null }
1488
+ }
1489
+
1490
+ // DSH 凭据库第三通道(旧 core readDshWorkbuddyToken verbatim 语义;CODEBUDDY_
1491
+ // CREDENTIALS_DIR 覆盖测试夹具;fs 经 deps 注入)。
1492
+ function readDshWorkbuddyToken(io) {
1493
+ const e = (io && io.env) || cbCatalogEnv
1494
+ const read = (io && io.readFileSync) || cbCatalogRead
1495
+ const overrideDir = e.CODEBUDDY_CREDENTIALS_DIR || null
1496
+ const home = overrideDir || e.DSH_HOME || null
1497
+ if (!home) return null
1498
+ const files = [home + '\\.credentials.yaml', home + '\\.env', home + '/.credentials.yaml', home + '/.env']
1499
+ for (const f of files) {
1500
+ const text = read(f)
1501
+ if (!text) continue
1502
+ for (const key of CREDENTIAL_KEYS) {
1503
+ const m = text.match(new RegExp('^[ \\t]*' + key + '[ \\t]*[:=][ \\t]*(.+)$', 'm'))
1504
+ if (!m) continue
1505
+ const v = m[1].trim().replace(/\s+#.*$/, '').replace(/^['"]|['"]$/g, '').trim()
1506
+ // 只接受看起来是真实 token 的值(避免把 $VAR 引用或占位符当凭据)。
1507
+ if (v.length >= 40 && !v.startsWith('$') && !/^<.*>$/.test(v)) return v
1508
+ }
1509
+ }
1510
+ return null
1511
+ }
1512
+
1513
+ // codebuddy-en 凭据链(旧 core verbatim):设置面板 > CODEBUDDY_AUTH_TOKEN >
1514
+ // DSH 凭据库。dynamic 沙箱无 fs → 第三通道自然缺席(旧 dynamic 本也有限)。
1515
+ function resolveEnToken(settingToken, io) {
1516
+ const e = (io && io.env) || cbCatalogEnv
1517
+ const s = String(settingToken || '').trim()
1518
+ if (s) return { token: s, source: 'setting' }
1519
+ const envTok = e.CODEBUDDY_AUTH_TOKEN || ''
1520
+ if (String(envTok).trim()) return { token: String(envTok).trim(), source: 'env' }
1521
+ const fromStore = readDshWorkbuddyToken(io)
1522
+ if (fromStore) return { token: fromStore, source: 'dsh-store' }
1523
+ return { token: null, source: null }
1524
+ }
1525
+
1733
1526
 
1734
1527
  // ── inlined from providers/codebuddy-consts.mjs ──
1735
- // providers/codebuddy-consts.mjs — codebuddy 的提示词策略与后端注册表常量(纯数据)。
1736
- //
1737
- // 从 providers/codebuddy.mjs 拆出,让主描述符留在 600 行红线内(规则 §26)。
1738
- // build 会把 providers/codebuddy-*.mjs 全部内联进 dynamic 沙箱同一作用域。
1739
- // 命名纪律:顶层名绝不与 core/bridge-kit.mjs 同名(同作用域重复声明 = 语法错)。
1740
- // ⚠️ 注释里绝不写出口令标记字面量(build 全局替换会把注释劈开)。
1741
-
1742
- // ── 提示词策略段(旧 preset POLICY_TEXT verbatim;systemPrompt order 5)────────
1743
- const POLICY_TEXT = [
1744
- 'codebuddy-first execution policy (local codebuddy CLI bridge, with fallback + live indicator + live status tool).',
1745
- '',
1746
- 'A local sub-agent CLI named "codebuddy" (Tencent CodeBuddy Code) is available through the codebuddy_run and codebuddy_continue tools. DSH fully controls codebuddy: every invocation runs non-interactively with permissions auto-approved (--permission-mode bypassPermissions), so codebuddy never prompts and DSH decides all of its work. A header status light shows when codebuddy is working — one light per project (working directory), so parallel runs in different projects are tracked separately; call codebuddy_status any time to see what codebuddy is doing right now (per project: current tool/step, recent trail, and cumulative usage).',
1747
- '',
1748
- 'Priority rule for EVERY mode (normal, plan, accept-edits, subagent/workflow/ralph delegation, and goal rounds):',
1749
- '- For any real work — implementing features, editing/refactoring code, debugging, running builds, or multi-file investigation — prefer dispatching the task to codebuddy_run FIRST with a complete, self-contained prompt.',
1750
- '- Use native read/write/shell tools mainly for quick read-only lookups and for the final build/test verification of what codebuddy produced.',
1751
- '- In plan mode, call codebuddy_run with mode=auto (or mode=plan); in accept-edits/normal mode codebuddy_run applies edits directly.',
1752
- '- When you delegate to a DSH subagent or workflow, instruct that delegate to also prefer codebuddy_run.',
1753
- '- For long-running tasks, call codebuddy_run with background=true and collect the result with job_output; use codebuddy_status to watch progress.',
1754
- '',
1755
- 'Fallback protocol: when codebuddy is rate-limited or the network is down, codebuddy_run/codebuddy_continue automatically pop a confirmation dialog asking the user whether to use the DSH local API config. If the returned result has fallback=true (status FALLBACK_TO_DSH), the user chose to fall back: complete the task with native DSH tools / the local model and DO NOT call codebuddy again for this task. If ok=false without fallback, report the codebuddy error. Never loop codebuddy calls; never ask codebuddy to call back into DSH.',
1756
- '',
1757
- 'Model selection: codebuddy_run takes an optional model. When unspecified, the CLI default applies unless the user set a preferred default model in the plugin settings (then that is injected automatically per call). Which models ACTUALLY WORK depends on the ACCOUNT, and the authoritative live list comes from the server: running the CLI with a nonexistent model returns 400 followed by "Currently supported models for your account:" and one "- <id>" per line (core exposes parseAccountModels for this; the plugin caches the result automatically and the settings panel shows exactly those ids, with the credit multiplier of each). Do NOT trust the bundled product descriptor as a menu of usable models: it lists ids with an "-ioa" suffix (glm-5.3-ioa) and expensive flagships (claude-opus-4.8, gpt-5.4, gpt-6-astra) that are present in the catalogue but NOT licensed for this account, and picking one fails with 400 "only available for authorized users". The four backends are also not one pool: "codebuddy" and "codebuddy-intl" are the npm CLI and offer its catalogue - including the free tier (hy3 and deepseek-v4.1-flash cost x0.00 credits; hy4-preview costs x0.29 but is frequently free on promotional accounts); "codebuddy-en" (WorkBuddyAI) has its OWN catalogue which does NOT contain hy4-preview or deepseek-v4.1-flash - it offers hy3 (free), gpt-5.1-codex-mini, gemini-3.1-flash-lite, gemini-2.5-flash, minimax-m3, gemini-3.0-flash, deepseek-v3-2-volc, kimi-k2.5/2.6, glm-5.0/5.2/5.3, gpt-5.3-codex, gpt-5.4/5.5, gpt-5.6-sol/terra/luna and the role aliases (fast/balanced/primary/deep-model); "workbuddy" (domestic desktop) has a further catalogue (auto, hy4-preview, hy3, deepseek-v4.1-flash, glm-5.x, kimi-k2.6/2.7/2.8-preview/k3-1, minimax-m3/m2.7, deepseek-v4-pro). Within a face availability is still gated per ACCOUNT: an id outside the account catalogue fails with "400 model [...] service info not found", and one inside it but unlicensed fails with "400 model [...] is only available for authorized users". International ids (claude-sonnet-5, claude-opus-5, gemini-3.1-pro, ...) are per-account gated and return the latter for a domestic account. COST: every id carries a credit multiplier in the credits field of its descriptor (x0.00 means free, x3.31 means 3.31x the base rate); the settings panel shows each candidate with its multiplier and reads it live from the CLI descriptor, so it stays in sync when the CLI is upgraded. Measured free (x0.00) models for this account: hy3 and deepseek-v4.1-flash (both npm faces) and hy3 for WorkBuddyAI; hy4-preview is x0.29 on npm faces. Prefer the cheapest model that can do the job and use a free one for routine work; do not silently upgrade to an expensive id when the task is simple. To get the exact live list for a CLI, run it with a deliberately invalid model id - the reply prints "Currently supported models for your account:" followed by one "- <id>" per line (core exposes parseAccountModels for this). Pass a model only when the task clearly benefits from a specific one; the default is usually right. Optional effort: minimal/low/medium/high/xhigh/max. Optional maxTurns caps agentic turns (default unlimited).',
1758
- '',
1759
- 'Backends: codebuddy_run/codebuddy_continue take an optional backend parameter choosing which CLI face of the same engine (Tencent CodeBuddy Code) runs the task. "codebuddy" is the domestic CodeBuddy (npm CLI @tencent-ai/codebuddy-code, product endpoint www.codebuddy.ai) — default for coding work. "codebuddy-intl" is the INTERNATIONAL face of that SAME npm CLI (product.ioa.json catalogue: claude-sonnet-5, claude-opus-5, gemini-3.1-pro, gpt-6-astra, hy3-ioa …); use it when the account is an international CodeBuddy account. Its aliases "codebuddy-ioa" and "codebuddy-international" are accepted and normalized to codebuddy-intl. "codebuddy-en" is the WorkBuddy INTERNATIONAL edition — the CLI bundled with the WorkBuddyAI desktop app (C:\\Program Files\\WorkBuddyAI, product endpoint www.workbuddy.ai); the aliases "workbuddy-en" and "workbuddy-ai" are also accepted and normalized to codebuddy-en. "workbuddy" is the CLI bundled with the domestic WorkBuddy desktop app (product endpoint copilot.tencent.com, zero config) — the office-scenario face: documents, slides, spreadsheets, knowledge-base lookups, image/video generation, WeChat/WeCom replies. When the user asks for office/document/IM work, dispatch with backend="workbuddy"; for international accounts use "codebuddy-intl" (CodeBuddy 国际版) or "codebuddy-en" (WorkBuddy 国际版 / WorkBuddyAI). Sessions are kept per backend (login domains are exclusive), and continuing a session automatically routes back to the backend that owns it (explicit backend wins). A user-preferred default backend (plugin settings) applies when a call is new (no session) and no explicit backend is given.'
1760
- ].join('\n')
1761
-
1762
- // ── 限流/网络/认证失败判定(旧 core verbatim)。仅匹配 stderr + status;数字码
1763
- // 加词边界(避免 "1500" 命中 500、"4013" 命中 401)。中文码(流量/受限/金额/
1764
- // 余额/额度/认证/连接/断开 等)为 codebuddy 面专属,必须逐字保留。
1765
- // 命名纪律(mimo 同款):本文件顶层名绝不与 core/bridge-kit.mjs 顶层导出同名 ——
1766
- // dynamic 形态把 kit 与 provider 内联进同一脚本作用域,同名 const 直接炸语法
1767
-
1768
- // ── 后端注册表(旧 core verbatim:端点按登录域自动对齐)───────────────────────
1769
- const CODEBUDDY_LIMIT_RE = /rate.?limit|ratelimit|\b429\b|too many|quota|insufficient|credit|balance|exhausted|exceed|\bnetwork\b|offline|ENETUNREACH|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|\btimeouts?\b|timed out|unavailable|\b50[023]\b|\b40[13]\b|unauthorized|invalid api|api key|proxy|socket|tls|ssl|\bdns\b|网络|超时|限流|流量|受限|配额|金额|余额|额度|认证|连接|断开/i
1770
-
1771
- // ── 后端注册表(旧 core verbatim:端点按登录域自动对齐)───────────────────────
1772
- const BACKENDS = ['codebuddy', 'codebuddy-intl', 'codebuddy-en', 'workbuddy']
1773
-
1774
- const BACKEND_ENDPOINTS = {
1775
- 'codebuddy': 'https://www.codebuddy.ai/v2',
1776
- 'codebuddy-intl': 'https://www.codebuddy.ai/v2',
1777
- 'codebuddy-en': 'https://www.workbuddy.ai/v2',
1778
- 'workbuddy': 'https://copilot.tencent.com/v2'
1779
- }
1780
-
1781
- const AUTH_DOMAIN_ENDPOINTS = {
1782
- 'www.codebuddy.cn': 'https://www.codebuddy.cn/v2',
1783
- 'www.codebuddy.ai': 'https://www.codebuddy.ai/v2',
1784
- 'www.workbuddy.ai': 'https://www.workbuddy.ai/v2',
1785
- 'copilot.tencent.com': 'https://copilot.tencent.com/v2'
1786
- }
1787
-
1788
- const BACKEND_LABELS = {
1789
- 'codebuddy': 'CodeBuddy 国内版(npm CLI)',
1790
- 'codebuddy-intl': 'CodeBuddy 国际版(npm CLI · 国际面)',
1791
- 'codebuddy-en': 'WorkBuddy 国际版(WorkBuddyAI 桌面 CLI)',
1792
- 'workbuddy': 'WorkBuddy 国内版(桌面 CLI)'
1793
- }
1794
-
1795
- const BACKEND_ALIASES = {
1796
- 'workbuddy-en': 'codebuddy-en',
1797
- 'workbuddy-ai': 'codebuddy-en',
1798
- 'codebuddy-ioa': 'codebuddy-intl',
1799
- 'codebuddy-international': 'codebuddy-intl',
1800
- 'codebuddy-oversea': 'codebuddy-intl'
1801
- }
1802
-
1803
- const DEFAULT_BACKEND = 'codebuddy'
1804
-
1805
- const SETTINGS_KEYS = ['preferredBackend', 'defaultModel', 'codebuddyEnToken', 'endpointOverride']
1806
-
1528
+ // providers/codebuddy-consts.mjs — codebuddy 的提示词策略与后端注册表常量(纯数据)。
1529
+ //
1530
+ // 从 providers/codebuddy.mjs 拆出,让主描述符留在 600 行红线内(规则 §26)。
1531
+ // build 会把 providers/codebuddy-*.mjs 全部内联进 dynamic 沙箱同一作用域。
1532
+ // 命名纪律:顶层名绝不与 core/bridge-kit.mjs 同名(同作用域重复声明 = 语法错)。
1533
+ // ⚠️ 注释里绝不写出口令标记字面量(build 全局替换会把注释劈开)。
1534
+
1535
+ // ── 提示词策略段(旧 preset POLICY_TEXT verbatim;systemPrompt order 5)────────
1536
+ const POLICY_TEXT = [
1537
+ 'codebuddy-first execution policy (local codebuddy CLI bridge, with fallback + live indicator + live status tool).',
1538
+ '',
1539
+ 'A local sub-agent CLI named "codebuddy" (Tencent CodeBuddy Code) is available through the codebuddy_run and codebuddy_continue tools. DSH fully controls codebuddy: every invocation runs non-interactively with permissions auto-approved (--permission-mode bypassPermissions), so codebuddy never prompts and DSH decides all of its work. A header status light shows when codebuddy is working — one light per project (working directory), so parallel runs in different projects are tracked separately; call codebuddy_status any time to see what codebuddy is doing right now (per project: current tool/step, recent trail, and cumulative usage).',
1540
+ '',
1541
+ 'Priority rule for EVERY mode (normal, plan, accept-edits, subagent/workflow/ralph delegation, and goal rounds):',
1542
+ '- For any real work — implementing features, editing/refactoring code, debugging, running builds, or multi-file investigation — prefer dispatching the task to codebuddy_run FIRST with a complete, self-contained prompt.',
1543
+ '- Use native read/write/shell tools mainly for quick read-only lookups and for the final build/test verification of what codebuddy produced.',
1544
+ '- In plan mode, call codebuddy_run with mode=auto (or mode=plan); in accept-edits/normal mode codebuddy_run applies edits directly.',
1545
+ '- When you delegate to a DSH subagent or workflow, instruct that delegate to also prefer codebuddy_run.',
1546
+ '- For long-running tasks, call codebuddy_run with background=true and collect the result with job_output; use codebuddy_status to watch progress.',
1547
+ '',
1548
+ 'Fallback protocol: when codebuddy is rate-limited or the network is down, codebuddy_run/codebuddy_continue automatically pop a confirmation dialog asking the user whether to use the DSH local API config. If the returned result has fallback=true (status FALLBACK_TO_DSH), the user chose to fall back: complete the task with native DSH tools / the local model and DO NOT call codebuddy again for this task. If ok=false without fallback, report the codebuddy error. Never loop codebuddy calls; never ask codebuddy to call back into DSH.',
1549
+ '',
1550
+ 'Model selection: codebuddy_run takes an optional model. When unspecified, the CLI default applies unless the user set a preferred default model in the plugin settings (then that is injected automatically per call). Which models ACTUALLY WORK depends on the ACCOUNT, and the authoritative live list comes from the server: running the CLI with a nonexistent model returns 400 followed by "Currently supported models for your account:" and one "- <id>" per line (core exposes parseAccountModels for this; the plugin caches the result automatically and the settings panel shows exactly those ids, with the credit multiplier of each). Do NOT trust the bundled product descriptor as a menu of usable models: it lists ids with an "-ioa" suffix (glm-5.3-ioa) and expensive flagships (claude-opus-4.8, gpt-5.4, gpt-6-astra) that are present in the catalogue but NOT licensed for this account, and picking one fails with 400 "only available for authorized users". The four backends are also not one pool: "codebuddy" and "codebuddy-intl" are the npm CLI and offer its catalogue - including the free tier (hy3 and deepseek-v4.1-flash cost x0.00 credits; hy4-preview costs x0.29 but is frequently free on promotional accounts); "codebuddy-en" (WorkBuddyAI) has its OWN catalogue which does NOT contain hy4-preview or deepseek-v4.1-flash - it offers hy3 (free), gpt-5.1-codex-mini, gemini-3.1-flash-lite, gemini-2.5-flash, minimax-m3, gemini-3.0-flash, deepseek-v3-2-volc, kimi-k2.5/2.6, glm-5.0/5.2/5.3, gpt-5.3-codex, gpt-5.4/5.5, gpt-5.6-sol/terra/luna and the role aliases (fast/balanced/primary/deep-model); "workbuddy" (domestic desktop) has a further catalogue (auto, hy4-preview, hy3, deepseek-v4.1-flash, glm-5.x, kimi-k2.6/2.7/2.8-preview/k3-1, minimax-m3/m2.7, deepseek-v4-pro). Within a face availability is still gated per ACCOUNT: an id outside the account catalogue fails with "400 model [...] service info not found", and one inside it but unlicensed fails with "400 model [...] is only available for authorized users". International ids (claude-sonnet-5, claude-opus-5, gemini-3.1-pro, ...) are per-account gated and return the latter for a domestic account. COST: every id carries a credit multiplier in the credits field of its descriptor (x0.00 means free, x3.31 means 3.31x the base rate); the settings panel shows each candidate with its multiplier and reads it live from the CLI descriptor, so it stays in sync when the CLI is upgraded. Measured free (x0.00) models for this account: hy3 and deepseek-v4.1-flash (both npm faces) and hy3 for WorkBuddyAI; hy4-preview is x0.29 on npm faces. Prefer the cheapest model that can do the job and use a free one for routine work; do not silently upgrade to an expensive id when the task is simple. To get the exact live list for a CLI, run it with a deliberately invalid model id - the reply prints "Currently supported models for your account:" followed by one "- <id>" per line (core exposes parseAccountModels for this). Pass a model only when the task clearly benefits from a specific one; the default is usually right. Optional effort: minimal/low/medium/high/xhigh/max. Optional maxTurns caps agentic turns (default unlimited).',
1551
+ '',
1552
+ 'Backends: codebuddy_run/codebuddy_continue take an optional backend parameter choosing which CLI face of the same engine (Tencent CodeBuddy Code) runs the task. "codebuddy" is the domestic CodeBuddy (npm CLI @tencent-ai/codebuddy-code, product endpoint www.codebuddy.ai) — default for coding work. "codebuddy-intl" is the INTERNATIONAL face of that SAME npm CLI (product.ioa.json catalogue: claude-sonnet-5, claude-opus-5, gemini-3.1-pro, gpt-6-astra, hy3-ioa …); use it when the account is an international CodeBuddy account. Its aliases "codebuddy-ioa" and "codebuddy-international" are accepted and normalized to codebuddy-intl. "codebuddy-en" is the WorkBuddy INTERNATIONAL edition — the CLI bundled with the WorkBuddyAI desktop app (C:\\Program Files\\WorkBuddyAI, product endpoint www.workbuddy.ai); the aliases "workbuddy-en" and "workbuddy-ai" are also accepted and normalized to codebuddy-en. "workbuddy" is the CLI bundled with the domestic WorkBuddy desktop app (product endpoint copilot.tencent.com, zero config) — the office-scenario face: documents, slides, spreadsheets, knowledge-base lookups, image/video generation, WeChat/WeCom replies. When the user asks for office/document/IM work, dispatch with backend="workbuddy"; for international accounts use "codebuddy-intl" (CodeBuddy 国际版) or "codebuddy-en" (WorkBuddy 国际版 / WorkBuddyAI). Sessions are kept per backend (login domains are exclusive), and continuing a session automatically routes back to the backend that owns it (explicit backend wins). A user-preferred default backend (plugin settings) applies when a call is new (no session) and no explicit backend is given.'
1553
+ ].join('\n')
1554
+
1555
+ // ── 限流/网络/认证失败判定(旧 core verbatim)。仅匹配 stderr + status;数字码
1556
+ // 加词边界(避免 "1500" 命中 500、"4013" 命中 401)。中文码(流量/受限/金额/
1557
+ // 余额/额度/认证/连接/断开 等)为 codebuddy 面专属,必须逐字保留。
1558
+ // 命名纪律(mimo 同款):本文件顶层名绝不与 core/bridge-kit.mjs 顶层导出同名 ——
1559
+ // dynamic 形态把 kit 与 provider 内联进同一脚本作用域,同名 const 直接炸语法
1560
+
1561
+ // ── 后端注册表(旧 core verbatim:端点按登录域自动对齐)───────────────────────
1562
+ const CODEBUDDY_LIMIT_RE = /rate.?limit|ratelimit|\b429\b|too many|quota|insufficient|credit|balance|exhausted|exceed|\bnetwork\b|offline|ENETUNREACH|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|\btimeouts?\b|timed out|unavailable|\b50[023]\b|\b40[13]\b|unauthorized|invalid api|api key|proxy|socket|tls|ssl|\bdns\b|网络|超时|限流|流量|受限|配额|金额|余额|额度|认证|连接|断开/i
1563
+
1564
+ // ── 后端注册表(旧 core verbatim:端点按登录域自动对齐)───────────────────────
1565
+ const BACKENDS = ['codebuddy', 'codebuddy-intl', 'codebuddy-en', 'workbuddy']
1566
+
1567
+ const BACKEND_ENDPOINTS = {
1568
+ 'codebuddy': 'https://www.codebuddy.ai/v2',
1569
+ 'codebuddy-intl': 'https://www.codebuddy.ai/v2',
1570
+ 'codebuddy-en': 'https://www.workbuddy.ai/v2',
1571
+ 'workbuddy': 'https://copilot.tencent.com/v2'
1572
+ }
1573
+
1574
+ const AUTH_DOMAIN_ENDPOINTS = {
1575
+ 'www.codebuddy.cn': 'https://www.codebuddy.cn/v2',
1576
+ 'www.codebuddy.ai': 'https://www.codebuddy.ai/v2',
1577
+ 'www.workbuddy.ai': 'https://www.workbuddy.ai/v2',
1578
+ 'copilot.tencent.com': 'https://copilot.tencent.com/v2'
1579
+ }
1580
+
1581
+ const BACKEND_LABELS = {
1582
+ 'codebuddy': 'CodeBuddy 国内版(npm CLI)',
1583
+ 'codebuddy-intl': 'CodeBuddy 国际版(npm CLI · 国际面)',
1584
+ 'codebuddy-en': 'WorkBuddy 国际版(WorkBuddyAI 桌面 CLI)',
1585
+ 'workbuddy': 'WorkBuddy 国内版(桌面 CLI)'
1586
+ }
1587
+
1588
+ const BACKEND_ALIASES = {
1589
+ 'workbuddy-en': 'codebuddy-en',
1590
+ 'workbuddy-ai': 'codebuddy-en',
1591
+ 'codebuddy-ioa': 'codebuddy-intl',
1592
+ 'codebuddy-international': 'codebuddy-intl',
1593
+ 'codebuddy-oversea': 'codebuddy-intl'
1594
+ }
1595
+
1596
+ const DEFAULT_BACKEND = 'codebuddy'
1597
+
1598
+ const SETTINGS_KEYS = ['preferredBackend', 'defaultModel', 'codebuddyEnToken', 'endpointOverride']
1807
1599
 
1600
+
1601
+ // ── inlined from providers/codebuddy-models-source.mjs ──
1602
+ // providers/codebuddy-models-source.mjs — codebuddy 的「模型清单探测 + 缓存」(第二片).
1603
+ //
1604
+ // 从 codebuddy-catalog.mjs 抽出:探测要走子进程(同步/异步两条路),与纯解析逻辑内聚度低,
1605
+ // 分开后两个文件都在 600 行红线内(规则 §26)。scripts/build.mjs 会把 providers/codebuddy-*.mjs
1606
+ // 全部按字典序内联进 dynamic 沙箱同一作用域,所以这里仍可引用 catalog 的函数;ESM 形态走 import。
1607
+ // 注意:注释里绝不写出口令标记字面量(build 全局替换会把注释劈开导致生成物语法崩)。
1608
+ // import { cbCatalogEnv, cbCatalogRead, cbCatalogPathJoin, parseAccountModels } from './codebuddy-catalog.mjs'
1609
+ // ── 实时模型清单(CLI 是一手权威来源,对齐 workbuddy-auth 的「权威白名单」思路)──
1610
+ //
1611
+ // 为什么不再以静态表/磁盘描述文件为主:实测本机 codebuddy CLI --
1612
+ // • 账号实时清单(用无效模型触发 400 报文)= 19 项
1613
+ // • CLI `--help` 声明的 roster = 19 项,与账号清单完全一致
1614
+ // • 我们原先读的 product.ioa.json 静态表 = 16 项,且 2 个 id 已过时
1615
+ // (minimax-m3 → minimax-m3-pay、kimi-k3-1 → kimi-k3-2),另缺 4 个新模型。
1616
+ // 静态表是「随包冻结」的,必然落后于 CLI,故只保留作倍率元数据与最后兜底。
1617
+ //
1618
+ // 两级实时来源(都不静默冒充权威数据,各带 source 标签):
1619
+ // A. account-live —— 起 CLI 用无效模型探测「该账号当前支持」的清单(需该面已登录,
1620
+ // 一次网络往返;对照 workbuddy-auth 的 agents[cli].models 白名单语义)。
1621
+ // B. cli-help —— 解析 `--help` 的 "Currently supported: (...)"(零网络、随 CLI
1622
+ // 版本自动更新;产品面级 roster)。
1623
+ // 静态表仅作 credit 元数据与 descriptor-static 兜底标签,绝不冒充实时。
1624
+
1625
+ /** 解析 CLI `--help` 里声明的模型 roster。空/畸形输入返回 []。
1626
+ * ⚠️ 名册很长时 yargs 会在任意列**折行**(实测把 `minimax-m3-pay` 从中间断成
1627
+ * `minimax-` + `m3-pay`),若按行/按逗号直接切会丢项或产生残片(同一命令两次运行
1628
+ * 给出 18/19 两种结果)。故先去掉括号内所有空白再按逗号切:逗号是唯一分隔符,
1629
+ * 折行插入的空白一律视为噪声。 */
1630
+ function parseHelpModels(helpText) {
1631
+ const m = /Currently supported:\s*\(([^)]+)\)/.exec(String(helpText || ''))
1632
+ if (!m) return []
1633
+ const out = []
1634
+ for (const raw of m[1].replace(/\s+/g, '').split(',')) {
1635
+ const id = raw.trim()
1636
+ if (id && /^[A-Za-z0-9_.\-]+$/.test(id) && out.indexOf(id) < 0) out.push(id)
1637
+ }
1638
+ return out
1639
+ }
1640
+
1641
+ /** 解析 codebuddy CLI 的可执行候选(env > npm 全局 shim > PATH 名)。 */
1642
+ function cbCatalogCliCandidates(env) {
1643
+ const e = env || cbCatalogEnv || {}
1644
+ const out = []
1645
+ const push = (p) => { if (p && out.indexOf(p) < 0) out.push(p) }
1646
+ if (e.CODEBUDDY_BIN) push(String(e.CODEBUDDY_BIN))
1647
+ const joinP = (a, b) => cbCatalogPathJoin(a, b)
1648
+ if (e.APPDATA) push(joinP(joinP(e.APPDATA, 'npm'), 'codebuddy.cmd'))
1649
+ if (e.APPDATA) push(joinP(joinP(e.APPDATA, 'npm'), 'codebuddy'))
1650
+ push('codebuddy')
1651
+ return out
1652
+ }
1653
+
1654
+ const CLI_MODEL_CACHE = new Map() // backend → { at, mode, ids }
1655
+ const CLI_MODEL_TTL_MS = 5 * 60 * 1000
1656
+
1657
+ // Windows .cmd/.bat 需要经 shell(Node 安全限制),但 `shell:true` + args 数组会触发
1658
+ // DEP0190(参数不转义)。这里统一做法:把命令与参数拼成【一个】字符串交给 shell,
1659
+ // 参数各自按 Windows 规则加引号 —— 既避开 DEP0190,又保证含空格参数不被拆开。
1660
+ function quoteWinArg(a) {
1661
+ const s = String(a)
1662
+ if (s === '') return '""'
1663
+ if (!/[\s"&|<>^]/.test(s)) return s
1664
+ return '"' + s.replace(/"/g, '\\"') + '"'
1665
+ }
1666
+ function cliCommandLine(exe, args) {
1667
+ return [exe].concat(args || []).map(quoteWinArg).join(' ')
1668
+ }
1669
+
1670
+ /** 把候选拆成 { cmd, prefixArgs }:.mjs/.js/.cjs 目标经 node 包装(测试夹具与自定义
1671
+ * 入口都靠这条),其余原样。 */
1672
+ function splitExeSpec(exe) {
1673
+ if (/\.(mjs|cjs|js)$/i.test(String(exe))) {
1674
+ const proc = globalThis.process
1675
+ return { cmd: (proc && proc.execPath) ? proc.execPath : 'node', prefixArgs: [String(exe)] }
1676
+ }
1677
+ return { cmd: String(exe), prefixArgs: [] }
1678
+ }
1679
+
1680
+ function cliSpawnSync(exe, args, opts) {
1681
+ const proc = globalThis.process
1682
+ const child = (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule('node:child_process') : null
1683
+ if (!child || typeof child.spawnSync !== 'function') return null
1684
+ const base = Object.assign({
1685
+ encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe']
1686
+ }, opts || {})
1687
+ const spec = splitExeSpec(exe)
1688
+ const argv = spec.prefixArgs.concat(args || [])
1689
+ const isCmd = /\.(cmd|bat)$/i.test(spec.cmd)
1690
+ if (isCmd) base.shell = true
1691
+ try {
1692
+ // shell 形态不带 args 数组(否则 DEP0190);非 shell 形态正常传数组。
1693
+ return isCmd
1694
+ ? child.spawnSync(cliCommandLine(spec.cmd, argv), base)
1695
+ : child.spawnSync(spec.cmd, argv, base)
1696
+ } catch (e) { return null }
1697
+ }
1698
+
1699
+ /**
1700
+ * 同步取「CLI 声明的 roster」(零网络,随 CLI 版本走)。
1701
+ * @returns {{ok:boolean, ids:string[], mode:string, error?:string}}
1702
+ */
1703
+ function fetchHelpModelsSync(env, opts) {
1704
+ const o = opts || {}
1705
+ const cands = cbCatalogCliCandidates(env)
1706
+ let lastErr = null
1707
+ for (const exe of cands) {
1708
+ const r = cliSpawnSync(exe, ['--help'], { timeout: Math.max(1000, Math.min(o.timeoutMs || 12000, 30000)) })
1709
+ if (!r || r.error) { lastErr = 'spawn failed: ' + String((r && r.error && r.error.message) || 'unavailable'); continue }
1710
+ const text = String(r.stdout || '') + String(r.stderr || '')
1711
+ const ids = parseHelpModels(text)
1712
+ if (ids.length) return { ok: true, ids, mode: 'cli-help', exe }
1713
+ lastErr = 'no roster in --help output'
1714
+ }
1715
+ return { ok: false, ids: [], mode: 'cli-help', error: lastErr || 'codebuddy CLI not found (set CODEBUDDY_BIN)' }
1716
+ }
1717
+
1718
+ /**
1719
+ * 同步版的账号探测(供设置面板用 —— 面板必须同步返回)。
1720
+ * 走 spawnSync + 超时;结果进 5 分钟缓存,避免每次渲染都打一次网络。
1721
+ *
1722
+ * ⚠️ 为什么面板不能用 `--help` 名册顶替:实测本机 CLI 2.167.0 的 help 文本**本身
1723
+ * 就是过时的** —— 它列 18 项且含已不存在的 `minimax-m3` / `kimi-k3-1`,缺真实的
1724
+ * `glm-5.3-flashx` / `minimax-m3-pay` / `kimi-k3-2`;而账号探测给出 19 项且全部有效。
1725
+ * 也就是说「CLI 声明」≠「服务端实际可用」,拿它当兜底会直接把错误清单展示给用户。
1726
+ * 故这里宁可空列表 + 明确提示,也不展示已知错误的 roster。
1727
+ * @returns {{ok:boolean, ids:string[], mode:string, error?:string}}
1728
+ */
1729
+ function probeAccountModelsSync(env, opts) {
1730
+ const o = opts || {}
1731
+ const cands = cbCatalogCliCandidates(env)
1732
+ const extraEnv = o.envExtra || {}
1733
+ const reasons = []
1734
+ const timeout = Math.max(3000, Math.min(o.timeoutMs || 12000, 30000))
1735
+ for (const exe of cands) {
1736
+ const r = cliSpawnSync(exe, ['-p', 'hi', '--model', '__clb_probe_invalid__', '--output-format', 'text'], {
1737
+ timeout,
1738
+ env: Object.assign({}, (globalThis.process && globalThis.process.env) || {}, env || {}, extraEnv)
1739
+ })
1740
+ if (!r || r.error) { reasons.push(exe + ': ' + String((r && r.error && (r.error.code || r.error.message)) || 'spawn failed')); continue }
1741
+ const out = String(r.stdout || '') + String(r.stderr || '')
1742
+ const ids = parseAccountModels(out)
1743
+ if (ids && ids.length) return { ok: true, ids, mode: 'account-live', exe }
1744
+ const head = String(out).replace(/\s+/g, ' ').trim().slice(0, 140)
1745
+ reasons.push(exe + ': exit ' + r.status + ', no account roster' + (head ? ' — ' + head : ''))
1746
+ }
1747
+ return { ok: false, ids: [], mode: 'account-live', error: reasons.join('; ') || 'no CLI candidate to probe' }
1748
+ }
1749
+
1750
+ /**
1751
+ * 异步探测「该账号当前支持」的清单:用无效模型触发 CLI 的 400 报文。
1752
+ * 对齐 workbuddy-auth 的权威白名单语义(服务器告诉我们这个账号能用什么)。
1753
+ * @returns {Promise<{ok:boolean, ids:string[], mode:string, error?:string}>}
1754
+ */
1755
+ function probeAccountModels(env, opts) {
1756
+ const o = opts || {}
1757
+ const cands = cbCatalogCliCandidates(env)
1758
+ const proc = globalThis.process
1759
+ const child = (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule('node:child_process') : null
1760
+ if (!child || typeof child.spawn !== 'function') {
1761
+ return Promise.resolve({ ok: false, ids: [], mode: 'account-live', error: 'no spawn capability in this host form' })
1762
+ }
1763
+ const extraEnv = o.envExtra || {}
1764
+ return new Promise((resolve) => {
1765
+ let idx = 0
1766
+ // 记录每个候选的真实结局:区分「压根没找到 CLI」与「CLI 跑了但没吐账号名录」
1767
+ // (后者通常是该面未登录 / 端点拒了)—— 报错文案必须能指导下一步,不能一律
1768
+ // 说 "CLI not found"(本轮就踩到:codebuddy-en 无 token 时被误报成找不到 CLI)。
1769
+ const reasons = []
1770
+ const attempt = () => {
1771
+ if (idx >= cands.length) {
1772
+ resolve({
1773
+ ok: false, ids: [], mode: 'account-live',
1774
+ error: reasons.length ? reasons.join('; ') : 'no CLI candidate to probe',
1775
+ notFound: reasons.every((r) => /not found|ENOENT|spawn/i.test(r))
1776
+ })
1777
+ return
1778
+ }
1779
+ const exe = cands[idx++]
1780
+ let c
1781
+ try {
1782
+ // .cmd/.bat 需经 shell(Windows 安全限制);.mjs/.js 经 node 包装。
1783
+ // 与 cliSpawnSync 同一规则:shell 形态把命令拼成单字符串(避免 DEP0190)。
1784
+ const spec = splitExeSpec(exe)
1785
+ const isCmd = /\.(cmd|bat)$/i.test(spec.cmd)
1786
+ const probeArgs = spec.prefixArgs.concat(['-p', 'hi', '--model', '__clb_probe_invalid__', '--output-format', 'text'])
1787
+ const opts = {
1788
+ stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
1789
+ env: Object.assign({}, proc.env, env || {}, extraEnv)
1790
+ }
1791
+ c = isCmd
1792
+ ? child.spawn(cliCommandLine(spec.cmd, probeArgs), Object.assign({ shell: true }, opts))
1793
+ : child.spawn(spec.cmd, probeArgs, opts)
1794
+ } catch (e) { reasons.push(exe + ': spawn threw ' + String(e && e.message || e)); attempt(); return }
1795
+ let out = ''
1796
+ const timer = setTimeout(() => { try { c.kill() } catch (e) { } }, Math.max(5000, Math.min(o.timeoutMs || 30000, 90000)))
1797
+ const onData = (d) => { out += d; if (out.length > 400000) out = out.slice(-200000) }
1798
+ c.stdout.on('data', onData)
1799
+ c.stderr.on('data', onData)
1800
+ c.on('error', (e) => {
1801
+ clearTimeout(timer)
1802
+ reasons.push(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
1803
+ attempt()
1804
+ })
1805
+ c.on('close', (code) => {
1806
+ clearTimeout(timer)
1807
+ const ids = parseAccountModels(out)
1808
+ if (ids && ids.length) { resolve({ ok: true, ids, mode: 'account-live', exe }); return }
1809
+ const head = String(out).replace(/\s+/g, ' ').trim().slice(0, 160)
1810
+ reasons.push(exe + ': exit ' + code + ', no account roster' + (head ? ' — ' + head : ''))
1811
+ attempt()
1812
+ })
1813
+ }
1814
+ attempt()
1815
+ })
1816
+ }
1817
+
1818
+ /** 缓存读写(面板每次渲染都会取,避免反复起进程)。 */
1819
+ function cachedCliModels(backend, load, nowMs) {
1820
+ const at = Number(nowMs) || Date.now()
1821
+ const hit = CLI_MODEL_CACHE.get(backend)
1822
+ if (hit && at - hit.at < CLI_MODEL_TTL_MS) return hit
1823
+ // 不传 load = 纯只读缓存(未命中返回 undefined)。设置面板靠这条避免每次渲染都 spawn。
1824
+ if (typeof load !== 'function') return undefined
1825
+ const v = load()
1826
+ if (v && v.ok && v.ids && v.ids.length) CLI_MODEL_CACHE.set(backend, { at, mode: v.mode, ids: v.ids.slice() })
1827
+ return v
1828
+ }
1829
+
1830
+ /**
1831
+ * 手动拉取成功后回填缓存(契约 §3.2 的 refreshModels 用),让随后的 meta() 能直接命中。
1832
+ * 只接受非空 ids —— 空结果绝不入缓存,否则下次渲染会显示"有缓存但没内容"。
1833
+ */
1834
+ function rememberCliModels(backend, ids, mode, nowMs) {
1835
+ if (!Array.isArray(ids) || !ids.length) return false
1836
+ CLI_MODEL_CACHE.set(backend, { at: Number(nowMs) || Date.now(), mode: mode || 'account-live', ids: ids.slice() })
1837
+ return true
1838
+ }
1839
+
1840
+ /** 读统一设置文件里 codebuddy 分区的 preferredBackend(meta 需要它才知道列哪个面)。 */
1841
+ function currentPreferredBackend() {
1842
+ try {
1843
+ const e = cbCatalogEnv || {}
1844
+ const p = cbCatalogRead(e.CLI_BRIDGE_SETTINGS_FILE || cbCatalogPathJoin(
1845
+ e.DSH_HOME || (e.APPDATA ? cbCatalogPathJoin(e.APPDATA, 'DSH Desktop', 'dsh-home') : ''), 'cli-bridge-settings.json'))
1846
+ if (!p) return null
1847
+ const all = JSON.parse(p)
1848
+ const v = all && all.codebuddy && all.codebuddy.preferredBackend
1849
+ return typeof v === 'string' && v ? v : null
1850
+ } catch (err) { return null }
1851
+ }
1852
+
1853
+ // CLI 报错里解析「该账号当前支持的模型」(旧 core parseAccountModels verbatim)。
1808
1854
  // ── inlined from providers/codebuddy-models-tool.mjs ──
1809
- // providers/codebuddy-models-tool.mjs — codebuddy_models 工具定义(纯逻辑)。
1810
- //
1811
- // 从 codebuddy-catalog.mjs 再拆出:两个文件都要留在 600 行红线内(规则 §26)。
1812
- // scripts/build.mjs 会把 providers/codebuddy-*.mjs 全部内联进 dynamic 沙箱的同一个
1813
- // 作用域,所以这里仍可引用 catalog 的函数(沙箱内天然可见);ESM 形态走本文件的 import。
1814
- // ⚠️ 注释里绝不写出口令标记字面量(build 全局替换会劈开注释 → 生成物语法崩)。
1815
- // ── codebuddy_models 工具定义(搬自主文件;纯逻辑,依赖经 ctx 注入)────────────
1816
- // ctx = { env, deps, resolveBackend, modelCatalogDetailed, getAccountCatalog }
1817
- function codebuddyModelsTool(ctx) {
1818
- const c = ctx || {}
1819
- const deps = c.deps || {}
1820
- const env = c.env || {}
1821
- return {
1822
- name: 'codebuddy_models',
1823
- description: 'List the models the local codebuddy CLI can use for a given backend, read LIVE from the CLI. Primary source: an account probe (runs the CLI with a deliberately invalid model id, which makes the server print "Currently supported models for your account") — this is the authoritative licensed list for THIS account. If that face is not logged in, it falls back to the roster the installed CLI declares in --help and says so in the source field. Credit multipliers (x0.00 = free) come from the product descriptor as metadata. Returns ok:false with an actionable error when no live roster can be read — it never presents a frozen static table as if it were live.',
1824
- inputSchema: {
1825
- type: 'object', additionalProperties: false, required: [],
1826
- properties: {
1827
- backend: { type: 'string', enum: c.BACKEND_ENUM, description: 'Which product face to list (default: the preferred backend).' },
1828
- freeOnly: { type: 'boolean', description: 'Return only x0.00 (free) models.' },
1829
- authorizedOnly: { type: 'boolean', description: 'Return only ids the current account is licensed for (harvested from a prior run).' }
1830
- }
1831
- },
1832
- render: (args, value) => {
1833
- const v = value || {}
1834
- if (!v.ok) return [{ type: 'text', text: 'codebuddy_models failed: ' + String(v.error || 'unknown') }]
1835
- const list = Array.isArray(v.models) ? v.models : []
1836
- if (!list.length) return [{ type: 'text', text: 'codebuddy_models: no models matched (backend=' + (v.backend || '?') + ')' }]
1837
- const lines = ['codebuddy models [' + v.backend + '] (' + list.length + '):']
1838
- for (const m of list.slice(0, 40)) {
1839
- const flags = []
1840
- if (m.authorized) flags.push('authorized')
1841
- if (m.credits === 0) flags.push('free')
1842
- lines.push(' ' + m.id + (m.creditLabel ? ' ' + m.creditLabel : '') + (flags.length ? ' [' + flags.join(',') + ']' : ''))
1843
- }
1844
- if (list.length > 40) lines.push(' … and ' + (list.length - 40) + ' more')
1845
- // 只有一种来源:账号实时(服务端裁决)。不再有 cli-help 兜底分支,
1846
- // 因为 CLI 的 --help 名册实测会滞后服务端并含已废弃 id。
1847
- lines.push('source: account-live (authoritative for this account)')
1848
- if (v.note) lines.push('note: ' + v.note)
1849
- return [{ type: 'text', text: lines.join('\n') }]
1850
- },
1851
- async logic(args) {
1852
- const a = args || {}
1853
- const preferred = currentPreferredBackend() || (deps.settings && deps.settings.preferredBackend) || null
1854
- const backend = c.normalizeBackend(a.backend) || c.resolveBackend({}, null, preferred)
1855
-
1856
- // ① 账号实时(最权威,对照 workbuddy-auth 的 agents[cli] 白名单语义)。
1857
- // 该面未登录/无 token 时探测会失败 —— 失败就**明确降级到 CLI 名册并标注**,
1858
- // 绝不把静态表冒充成账号可用清单。
1859
- const extraEnv = (c.faceEnv && c.faceEnv(backend)) || {}
1860
- const probe = await probeAccountModels(env, { envExtra: extraEnv, timeoutMs: a.timeoutMs })
1861
-
1862
- // 唯一权威源 = 账号探测(服务端裁决)。**不再回落 CLI `--help` 名册**:
1863
- // 实测 CLI 2.167.0 的 help 文本本身就过时(18 项、含已废弃的 minimax-m3 /
1864
- // kimi-k3-1、缺 3 个真实模型),拿它兜底等于把错误清单当结果交付。
1865
- // 探测失败就明确失败,并附上「CLI 声明了 N 项」作为诊断线索(只作提示,不展示
1866
- // 那些 id),符合红线:开发期禁止静默降级。
1867
- if (!probe.ok || !probe.ids || !probe.ids.length) {
1868
- let hint = ''
1869
- try {
1870
- const help = fetchHelpModelsSync(env, { timeoutMs: a.timeoutMs })
1871
- if (help.ok && help.ids.length) {
1872
- hint = ' The installed CLI declares ' + help.ids.length + ' models via --help, but that roster is a static ' +
1873
- 'build-time list and has been observed to lag the server (it can contain retired ids). It is NOT shown here ' +
1874
- 'because it is not authoritative for this account.'
1875
- }
1876
- } catch (e) { }
1877
- return {
1878
- ok: false, backend,
1879
- error: 'no account model roster for backend "' + backend + '": ' + String(probe.error || 'probe failed') +
1880
- '. Make sure this product face is logged in (run the codebuddy CLI and complete login), or set CODEBUDDY_BIN ' +
1881
- 'to the right executable, then retry.' + hint
1882
- }
1883
- }
1884
- const ids = probe.ids
1885
- const source = 'account-live'
1886
-
1887
- // ③ 倍率/授权元数据(来自描述文件;仅作标注,不参与「是否可用」判定)。
1888
- let detail = []
1889
- try { detail = c.modelCatalogDetailed(backend, { env, accountModels: ids }) || [] } catch (e) { detail = [] }
1890
- const byId = new Map(detail.map((x) => [x.id, x]))
1891
- let list = ids.map((id) => {
1892
- const d = byId.get(id)
1893
- return {
1894
- id,
1895
- authorized: d ? !!d.authorized : (source === 'account-live'),
1896
- credits: d && typeof d.credits === 'number' ? d.credits : null,
1897
- creditLabel: (d && (d.creditLabel || d.label)) || '',
1898
- label: (d && d.label) || id
1899
- }
1900
- })
1901
- if (a.authorizedOnly) list = list.filter((m) => m.authorized)
1902
- if (a.freeOnly) list = list.filter((m) => m.credits === 0)
1903
- return {
1904
- ok: true, backend, models: list, count: list.length,
1905
- source, note: (typeof note !== 'undefined' ? note : undefined),
1906
- probedAt: Date.now()
1907
- }
1908
- }
1909
- }
1910
- }
1911
-
1855
+ // providers/codebuddy-models-tool.mjs — codebuddy_models 工具定义(纯逻辑)。
1856
+ //
1857
+ // 从 codebuddy-catalog.mjs 再拆出:两个文件都要留在 600 行红线内(规则 §26)。
1858
+ // scripts/build.mjs 会把 providers/codebuddy-*.mjs 全部内联进 dynamic 沙箱的同一个
1859
+ // 作用域,所以这里仍可引用 catalog 的函数(沙箱内天然可见);ESM 形态走本文件的 import。
1860
+ // ⚠️ 注释里绝不写出口令标记字面量(build 全局替换会劈开注释 → 生成物语法崩)。
1861
+ // import { currentPreferredBackend, probeAccountModels, fetchHelpModelsSync } from './codebuddy-models-source.mjs'
1862
+ // ── codebuddy_models 工具定义(搬自主文件;纯逻辑,依赖经 ctx 注入)────────────
1863
+ // ctx = { env, deps, resolveBackend, modelCatalogDetailed, getAccountCatalog }
1864
+ function codebuddyModelsTool(ctx) {
1865
+ const c = ctx || {}
1866
+ const deps = c.deps || {}
1867
+ const env = c.env || {}
1868
+ return {
1869
+ name: 'codebuddy_models',
1870
+ description: 'List the models the local codebuddy CLI can use for a given backend, read LIVE from the CLI. Primary source: an account probe (runs the CLI with a deliberately invalid model id, which makes the server print "Currently supported models for your account") — this is the authoritative licensed list for THIS account. If that face is not logged in, it falls back to the roster the installed CLI declares in --help and says so in the source field. Credit multipliers (x0.00 = free) come from the product descriptor as metadata. Returns ok:false with an actionable error when no live roster can be read — it never presents a frozen static table as if it were live.',
1871
+ inputSchema: {
1872
+ type: 'object', additionalProperties: false, required: [],
1873
+ properties: {
1874
+ backend: { type: 'string', enum: c.BACKEND_ENUM, description: 'Which product face to list (default: the preferred backend).' },
1875
+ freeOnly: { type: 'boolean', description: 'Return only x0.00 (free) models.' },
1876
+ authorizedOnly: { type: 'boolean', description: 'Return only ids the current account is licensed for (harvested from a prior run).' }
1877
+ }
1878
+ },
1879
+ render: (args, value) => {
1880
+ const v = value || {}
1881
+ if (!v.ok) return [{ type: 'text', text: 'codebuddy_models failed: ' + String(v.error || 'unknown') }]
1882
+ const list = Array.isArray(v.models) ? v.models : []
1883
+ if (!list.length) return [{ type: 'text', text: 'codebuddy_models: no models matched (backend=' + (v.backend || '?') + ')' }]
1884
+ const lines = ['codebuddy models [' + v.backend + '] (' + list.length + '):']
1885
+ for (const m of list.slice(0, 40)) {
1886
+ const flags = []
1887
+ if (m.authorized) flags.push('authorized')
1888
+ if (m.credits === 0) flags.push('free')
1889
+ lines.push(' ' + m.id + (m.creditLabel ? ' ' + m.creditLabel : '') + (flags.length ? ' [' + flags.join(',') + ']' : ''))
1890
+ }
1891
+ if (list.length > 40) lines.push(' … and ' + (list.length - 40) + ' more')
1892
+ // 只有一种来源:账号实时(服务端裁决)。不再有 cli-help 兜底分支,
1893
+ // 因为 CLI 的 --help 名册实测会滞后服务端并含已废弃 id。
1894
+ lines.push('source: account-live (authoritative for this account)')
1895
+ if (v.note) lines.push('note: ' + v.note)
1896
+ return [{ type: 'text', text: lines.join('\n') }]
1897
+ },
1898
+ async logic(args) {
1899
+ const a = args || {}
1900
+ const preferred = currentPreferredBackend() || (deps.settings && deps.settings.preferredBackend) || null
1901
+ const backend = c.normalizeBackend(a.backend) || c.resolveBackend({}, null, preferred)
1902
+
1903
+ // ① 账号实时(最权威,对照 workbuddy-auth 的 agents[cli] 白名单语义)。
1904
+ // 该面未登录/无 token 时探测会失败 —— 失败就**明确降级到 CLI 名册并标注**,
1905
+ // 绝不把静态表冒充成账号可用清单。
1906
+ const extraEnv = (c.faceEnv && c.faceEnv(backend)) || {}
1907
+ const probe = await probeAccountModels(env, { envExtra: extraEnv, timeoutMs: a.timeoutMs })
1908
+
1909
+ // 唯一权威源 = 账号探测(服务端裁决)。**不再回落 CLI `--help` 名册**:
1910
+ // 实测 CLI 2.167.0 的 help 文本本身就过时(18 项、含已废弃的 minimax-m3 /
1911
+ // kimi-k3-1、缺 3 个真实模型),拿它兜底等于把错误清单当结果交付。
1912
+ // 探测失败就明确失败,并附上「CLI 声明了 N 项」作为诊断线索(只作提示,不展示
1913
+ // 那些 id),符合红线:开发期禁止静默降级。
1914
+ if (!probe.ok || !probe.ids || !probe.ids.length) {
1915
+ let hint = ''
1916
+ try {
1917
+ const help = fetchHelpModelsSync(env, { timeoutMs: a.timeoutMs })
1918
+ if (help.ok && help.ids.length) {
1919
+ hint = ' The installed CLI declares ' + help.ids.length + ' models via --help, but that roster is a static ' +
1920
+ 'build-time list and has been observed to lag the server (it can contain retired ids). It is NOT shown here ' +
1921
+ 'because it is not authoritative for this account.'
1922
+ }
1923
+ } catch (e) { }
1924
+ return {
1925
+ ok: false, backend,
1926
+ error: 'no account model roster for backend "' + backend + '": ' + String(probe.error || 'probe failed') +
1927
+ '. Make sure this product face is logged in (run the codebuddy CLI and complete login), or set CODEBUDDY_BIN ' +
1928
+ 'to the right executable, then retry.' + hint
1929
+ }
1930
+ }
1931
+ const ids = probe.ids
1932
+ const source = 'account-live'
1933
+
1934
+ // ③ 倍率/授权元数据(来自描述文件;仅作标注,不参与「是否可用」判定)。
1935
+ let detail = []
1936
+ try { detail = c.modelCatalogDetailed(backend, { env, accountModels: ids }) || [] } catch (e) { detail = [] }
1937
+ const byId = new Map(detail.map((x) => [x.id, x]))
1938
+ let list = ids.map((id) => {
1939
+ const d = byId.get(id)
1940
+ return {
1941
+ id,
1942
+ authorized: d ? !!d.authorized : (source === 'account-live'),
1943
+ credits: d && typeof d.credits === 'number' ? d.credits : null,
1944
+ creditLabel: (d && (d.creditLabel || d.label)) || '',
1945
+ label: (d && d.label) || id
1946
+ }
1947
+ })
1948
+ if (a.authorizedOnly) list = list.filter((m) => m.authorized)
1949
+ if (a.freeOnly) list = list.filter((m) => m.credits === 0)
1950
+ return {
1951
+ ok: true, backend, models: list, count: list.length,
1952
+ source, note: (typeof note !== 'undefined' ? note : undefined),
1953
+ probedAt: Date.now()
1954
+ }
1955
+ }
1956
+ }
1957
+ }
1958
+
1912
1959
 
1913
1960
  // ── inlined from providers/codebuddy-schemas.mjs ──
1914
- // providers/codebuddy-schemas.mjs — 三工具 JSON Schema(纯数据,零依赖)。
1915
- //
1916
- // 旧 preset/codebuddy-first verbatim(契约 §8 红线):字段名/枚举/描述一字不改。
1917
- // 放这里是为了让 catalog 与主描述符都留在 600 行红线内(规则 §26)。
1918
- // ⚠️ 注释里绝不写出口令标记字面量(build 全局替换会把注释劈开)。
1919
- // ── 三工具 schema(旧 preset/codebuddy-first verbatim;契约 §8 红线)────────────
1920
- // 纯数据,无依赖 —— 放这里是为了让主描述符留在 600 行红线内(规则 §26)。
1921
- const RUN_PARAMS = {
1922
- type: 'object',
1923
- additionalProperties: false,
1924
- required: ['prompt'],
1925
- properties: {
1926
- prompt: { type: 'string', description: 'The full task/instruction for codebuddy. Be complete and self-contained.' },
1927
- backend: { type: 'string', enum: BACKEND_ENUM, description: 'Which CLI face to dispatch to. codebuddy (default; domestic CodeBuddy npm CLI, product endpoint www.codebuddy.ai) for coding work; codebuddy-intl = CodeBuddy INTERNATIONAL face of the same npm CLI (product.ioa.json catalogue: claude-sonnet-5 / claude-opus-5 / gemini-3.1-pro / gpt-6-astra / hy3-ioa ...; aliases codebuddy-ioa / codebuddy-international) when the account is an international CodeBuddy account; codebuddy-en = WorkBuddy international edition (the WorkBuddyAI desktop bundled CLI at C:\\Program Files\\WorkBuddyAI, product endpoint www.workbuddy.ai; needs codebuddyEnToken unless DSH already holds a workbuddy key) — aliases workbuddy-en / workbuddy-ai also accepted; workbuddy = WorkBuddy domestic desktop CLI (product endpoint copilot.tencent.com, zero config), the office-scenario face: documents/slides/spreadsheets, knowledge-base lookups, image/video generation, WeChat/WeCom replies. New calls without a backend follow the user-preferred default backend (plugin settings). Continuing a session routes back to its owning backend automatically.' },
1928
- mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'auto follows DSH plan state; plan = no writes; accept-edits = allow edits. Default auto.' },
1929
- model: { type: 'string', description: MODEL_DESCRIPTION },
1930
- effort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], description: 'Optional reasoning effort.' },
1931
- maxTurns: { type: 'integer', description: 'Optional max agentic turns (1-500, default unlimited).' },
1932
- cwd: { type: 'string', description: 'Working directory for codebuddy. Defaults to the DSH workspace root.' },
1933
- addDirs: { type: 'array', items: { type: 'string' }, description: 'Extra directories to add to codebuddy workspace.' },
1934
- timeoutSec: { type: 'integer', description: 'Run timeout seconds (10-3600, default 300); a DSH-side hang guard force-terminates at timeout+60s.' },
1935
- background: { type: 'boolean', description: 'Run as a background job and return a jobId immediately.' }
1936
- }
1937
- }
1938
- const CONTINUE_PARAMS = {
1939
- type: 'object',
1940
- additionalProperties: false,
1941
- required: ['prompt'],
1942
- properties: {
1943
- prompt: { type: 'string', description: 'Follow-up instruction for the ongoing codebuddy conversation.' },
1944
- sessionId: { type: 'string', description: 'codebuddy session id to resume (from a prior codebuddy_run result).' },
1945
- latest: { type: 'boolean', description: 'Continue the most recent codebuddy conversation instead of a specific id.' },
1946
- backend: { type: 'string', enum: BACKEND_ENUM, description: 'Which CLI face to resume on. codebuddy-en = WorkBuddy international (WorkBuddyAI desktop CLI; aliases workbuddy-en / workbuddy-ai accepted), workbuddy = WorkBuddy domestic desktop CLI. When omitted, the backend that owns the sessionId is used automatically; brand-new conversations follow the user-preferred default backend (plugin settings).' },
1947
- mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'Execution mode; default auto.' },
1948
- model: { type: 'string', description: 'Optional model id (two layers: product face carries it, account licenses it; the credit multiplier of each id comes from the CLI descriptor — see codebuddy_run).' },
1949
- effort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], description: 'Optional reasoning effort.' },
1950
- maxTurns: { type: 'integer', description: 'Optional max agentic turns.' },
1951
- cwd: { type: 'string', description: "Working directory for codebuddy; when resuming, defaults to the resumed session's project directory." },
1952
- timeoutSec: { type: 'integer', description: 'Run timeout seconds (10-3600, default 300); a DSH-side hang guard force-terminates at timeout+60s.' },
1953
- background: { type: 'boolean', description: 'Run as a background job and return a jobId immediately.' }
1954
- }
1955
- }
1956
- const STATUS_PARAMS = { type: 'object', additionalProperties: false, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
1961
+ // providers/codebuddy-schemas.mjs — 三工具 JSON Schema(纯数据,零依赖)。
1962
+ //
1963
+ // 旧 preset/codebuddy-first verbatim(契约 §8 红线):字段名/枚举/描述一字不改。
1964
+ // 放这里是为了让 catalog 与主描述符都留在 600 行红线内(规则 §26)。
1965
+ // ⚠️ 注释里绝不写出口令标记字面量(build 全局替换会把注释劈开)。
1966
+ // import { BACKEND_ENUM, MODEL_DESCRIPTION } from './codebuddy-catalog.mjs'
1967
+ // ── 三工具 schema(旧 preset/codebuddy-first verbatim;契约 §8 红线)────────────
1968
+ // 纯数据,无依赖 —— 放这里是为了让主描述符留在 600 行红线内(规则 §26)。
1969
+ const RUN_PARAMS = {
1970
+ type: 'object',
1971
+ additionalProperties: false,
1972
+ required: ['prompt'],
1973
+ properties: {
1974
+ prompt: { type: 'string', description: 'The full task/instruction for codebuddy. Be complete and self-contained.' },
1975
+ backend: { type: 'string', enum: BACKEND_ENUM, description: 'Which CLI face to dispatch to. codebuddy (default; domestic CodeBuddy npm CLI, product endpoint www.codebuddy.ai) for coding work; codebuddy-intl = CodeBuddy INTERNATIONAL face of the same npm CLI (product.ioa.json catalogue: claude-sonnet-5 / claude-opus-5 / gemini-3.1-pro / gpt-6-astra / hy3-ioa ...; aliases codebuddy-ioa / codebuddy-international) when the account is an international CodeBuddy account; codebuddy-en = WorkBuddy international edition (the WorkBuddyAI desktop bundled CLI at C:\\Program Files\\WorkBuddyAI, product endpoint www.workbuddy.ai; needs codebuddyEnToken unless DSH already holds a workbuddy key) — aliases workbuddy-en / workbuddy-ai also accepted; workbuddy = WorkBuddy domestic desktop CLI (product endpoint copilot.tencent.com, zero config), the office-scenario face: documents/slides/spreadsheets, knowledge-base lookups, image/video generation, WeChat/WeCom replies. New calls without a backend follow the user-preferred default backend (plugin settings). Continuing a session routes back to its owning backend automatically.' },
1976
+ mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'auto follows DSH plan state; plan = no writes; accept-edits = allow edits. Default auto.' },
1977
+ model: { type: 'string', description: MODEL_DESCRIPTION },
1978
+ effort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], description: 'Optional reasoning effort.' },
1979
+ maxTurns: { type: 'integer', description: 'Optional max agentic turns (1-500, default unlimited).' },
1980
+ cwd: { type: 'string', description: 'Working directory for codebuddy. Defaults to the DSH workspace root.' },
1981
+ addDirs: { type: 'array', items: { type: 'string' }, description: 'Extra directories to add to codebuddy workspace.' },
1982
+ timeoutSec: { type: 'integer', description: 'Run timeout seconds (10-3600, default 300); a DSH-side hang guard force-terminates at timeout+60s.' },
1983
+ background: { type: 'boolean', description: 'Run as a background job and return a jobId immediately.' }
1984
+ }
1985
+ }
1986
+ const CONTINUE_PARAMS = {
1987
+ type: 'object',
1988
+ additionalProperties: false,
1989
+ required: ['prompt'],
1990
+ properties: {
1991
+ prompt: { type: 'string', description: 'Follow-up instruction for the ongoing codebuddy conversation.' },
1992
+ sessionId: { type: 'string', description: 'codebuddy session id to resume (from a prior codebuddy_run result).' },
1993
+ latest: { type: 'boolean', description: 'Continue the most recent codebuddy conversation instead of a specific id.' },
1994
+ backend: { type: 'string', enum: BACKEND_ENUM, description: 'Which CLI face to resume on. codebuddy-en = WorkBuddy international (WorkBuddyAI desktop CLI; aliases workbuddy-en / workbuddy-ai accepted), workbuddy = WorkBuddy domestic desktop CLI. When omitted, the backend that owns the sessionId is used automatically; brand-new conversations follow the user-preferred default backend (plugin settings).' },
1995
+ mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'Execution mode; default auto.' },
1996
+ model: { type: 'string', description: 'Optional model id (two layers: product face carries it, account licenses it; the credit multiplier of each id comes from the CLI descriptor — see codebuddy_run).' },
1997
+ effort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], description: 'Optional reasoning effort.' },
1998
+ maxTurns: { type: 'integer', description: 'Optional max agentic turns.' },
1999
+ cwd: { type: 'string', description: "Working directory for codebuddy; when resuming, defaults to the resumed session's project directory." },
2000
+ timeoutSec: { type: 'integer', description: 'Run timeout seconds (10-3600, default 300); a DSH-side hang guard force-terminates at timeout+60s.' },
2001
+ background: { type: 'boolean', description: 'Run as a background job and return a jobId immediately.' }
2002
+ }
2003
+ }
2004
+ const STATUS_PARAMS = { type: 'object', additionalProperties: false, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
1957
2005
 
1958
2006
  // core/scheduling.mjs — CLI 调度(启用集合 + 调用优先级)单源纯逻辑。
1959
2007
  //