@whaletalk/dsh-codex-collab 0.1.2 → 0.1.4
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.
package/README.md
CHANGED
|
@@ -202,12 +202,17 @@ dsh-task --in "<继续推进的指令>" --session "session-8d481ad9-..."
|
|
|
202
202
|
|
|
203
203
|
## 已知限制
|
|
204
204
|
|
|
205
|
-
- 网关插件需要 `webServer` 服务(由 `dsh-web-app` 提供),因此**只能挂 web / desktop profile**;headless profile 会一直 pending。session
|
|
205
|
+
- 网关插件需要 `webServer` 服务(由 `dsh-web-app` 提供),因此**只能挂 web / desktop profile**;headless profile 会一直 pending。session 目标还需要 `sessionController`——它由组合**异步注册**(`webserver → web-runtime → connection → file-upload → session-controller`),所以插件是惰性取用,注册前调用会返回 `session-controller-unavailable`(稍后重试即可)
|
|
206
|
+
- **`sessionController.search` 可能被部署禁用**(`session-query` 索引 `openAt: "never"`)。此时按标题查找自动退回 `list`(读持久化 header + 标题投影,同样不激活 Agent),响应里的 `matchedBy` 标明走了哪条路
|
|
207
|
+
- **不能投递进子代理会话**:`resolveAgent` 对 subagent 路由拥有的会话返回 `session-busy: owned by subagent routing`;只能接普通会话
|
|
208
|
+
- `POST /api/dsh-bridge/sessions {"create": true, "cwd": "..."}` 可新建一条空白会话(探针/一次性任务用),它不会碰任何已有会话
|
|
206
209
|
- **同一会话同时只允许一个在途 bridge 任务**:DSH 自己会排队,但无法可靠地把两次并发派活的回报各自归属,所以第二个会被明确拒绝(`session-busy`)而不是猜
|
|
207
210
|
- **冷会话可能接不上**:DSH 激活 Agent 需要会话投影(`Agent activation requires a projected Session observation`),未激活的会话会返回 `session-not-activatable`;归档会话的回合会以 `blocked` 收口(`session-archived`)
|
|
208
211
|
- **评审不支持会话目标**:评审员必须与编码会话隔离,"独立评审"才成立
|
|
209
212
|
- session 目标不使用 `cwd`/`lane`/`model`/`--commit`(会话自带这些语义),传了会被判 `invalid-target`
|
|
210
213
|
- 网关重启会丢失内存中的任务表(含 session 观察器),这是既有设计
|
|
214
|
+
- **DSH profile 的 pnpm 供应链冷却**:新发布的版本约 24 小时内会被 `minimumReleaseAge` 拒绝安装(`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`);管理器会把它写进 `pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 才放行。刚发版就装不上属正常,不是包的问题
|
|
215
|
+
- 升级已装组合包 = **卸载 → 重新安装**;卸载会把该包从 `dsh.profile.bundles` 移除,所以重装后**要确认它被重新选中**,否则插件不会挂载(表现为所有 `/api/dsh-bridge/*` 都落到网关鉴权层返回 401)
|
|
211
216
|
- Codex Desktop Windows 本地市场技能注入 bug(见上,MCP 通道不受影响)
|
|
212
217
|
- 动态插件环境无 `AbortSignal`,冷恢复失败时自动降级为一次性执行;宿主组合持久化版无此问题
|
|
213
218
|
- ChatGPT Plus 写入型 MCP 的开放度取决于 OpenAI 产品策略
|
|
@@ -84,5 +84,7 @@ dsh-task --cancel <taskId> --force # 取消 session 目标任务(会打断
|
|
|
84
84
|
|
|
85
85
|
- 连接失败:DeepSeek Harness 未运行(网关应监听本机端口,默认 3080;挂在 web/desktop profile 时也可能就是 GUI 的端口)。
|
|
86
86
|
- 派 session 任务返回 `session-writer-held` / `session-busy`:那条会话被别的写入方或另一个 bridge 任务占用;稍后重试,或改用 worker 目标。
|
|
87
|
+
- 返回 `session-busy: owned by subagent routing`:那是子代理会话,**不能作为投递目标**;只能接普通会话。
|
|
87
88
|
- 返回 `session-not-activatable` / `session-archived`:该会话当前接不了(冷会话未能激活,或已归档);请用户先在客户端打开它,或解除归档。
|
|
89
|
+
- 某些部署禁用了搜索(会话索引 `openAt: never`):`--sessions` 会自动退回 `list` + 标题投影匹配,响应里的 `matchedBy` 会写 `list`。
|
|
88
90
|
- 读不到 `--cwd` 目录:提示用户在 Codex 设置中把该目录加入可访问/可写目录。
|
package/harness/dsh-bridge.mjs
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
classifyTarget,
|
|
30
30
|
selectSession,
|
|
31
31
|
mapControllerError,
|
|
32
|
+
sessionTitleOf,
|
|
33
|
+
looksLikeDisabledSearch,
|
|
32
34
|
taskResponse,
|
|
33
35
|
} from './session-target.mjs'
|
|
34
36
|
|
|
@@ -46,9 +48,11 @@ export async function apply(ctx) {
|
|
|
46
48
|
const sessionPersistence = ctx.get('sessionPersistence')
|
|
47
49
|
const workspaceRegistry = ctx.get('workspaceRegistry')
|
|
48
50
|
const llm = ctx.get('llm')
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
51
|
+
// 已有会话的读写入口。**必须惰性取**:@deepseek-ai/dsh-api-session-controller 的
|
|
52
|
+
// sessionController 由组合异步注册(DSH 启动日志里它先处于 waiting for services),
|
|
53
|
+
// 在 apply() 时捕获会永久拿到 undefined。每次使用时现取;缺失时返回稳定的
|
|
54
|
+
// session-controller-unavailable,而不是让整个插件挂掉。
|
|
55
|
+
const sessionControllerNow = () => ctx.get('sessionController')
|
|
52
56
|
|
|
53
57
|
// 清理历史版本可能泄漏的路由条目(重启前的动态插件、旧版本次插件)
|
|
54
58
|
try {
|
|
@@ -397,20 +401,66 @@ export async function apply(ctx) {
|
|
|
397
401
|
const sessionTargets = new Map()
|
|
398
402
|
|
|
399
403
|
const sessionSearch = async (query) => {
|
|
400
|
-
const
|
|
404
|
+
const controller = sessionControllerNow()
|
|
405
|
+
if (controller === undefined) {
|
|
406
|
+
throw Object.assign(new Error('sessionController service is not mounted in this profile'), { code: 'bridge/no-controller' })
|
|
407
|
+
}
|
|
408
|
+
const value = await controller.search({ query }, signalOf())
|
|
401
409
|
const items = value !== undefined && value !== null && Array.isArray(value.items) ? value.items : []
|
|
402
410
|
return items.filter((it) => it !== null && typeof it === 'object' && typeof it.sessionId === 'string' && it.sessionId !== '')
|
|
403
411
|
}
|
|
404
412
|
|
|
413
|
+
/**
|
|
414
|
+
* 退回路径:不打开 search 索引,直接遍历 list(读持久化 header + 投影缓存)。
|
|
415
|
+
* 有些部署把 session-query 索引配成 openAt: "never",此时 search 永远失败,
|
|
416
|
+
* 而按标题找会话仍必须可用——list 就是不激活 Agent 也能拿到标题的那条路。
|
|
417
|
+
*/
|
|
418
|
+
const sessionList = async (query) => {
|
|
419
|
+
const controller = sessionControllerNow()
|
|
420
|
+
if (controller === undefined) {
|
|
421
|
+
throw Object.assign(new Error('sessionController service is not mounted in this profile'), { code: 'bridge/no-controller' })
|
|
422
|
+
}
|
|
423
|
+
const needle = query.toLowerCase()
|
|
424
|
+
const value = await controller.list({}, signalOf())
|
|
425
|
+
const items = value !== undefined && value !== null && Array.isArray(value.items) ? value.items : []
|
|
426
|
+
const out = []
|
|
427
|
+
for (const summary of items) {
|
|
428
|
+
if (summary === null || typeof summary !== 'object' || typeof summary.sessionId !== 'string') continue
|
|
429
|
+
const title = sessionTitleOf(summary)
|
|
430
|
+
if (title !== '' && title.toLowerCase().includes(needle)) {
|
|
431
|
+
out.push({ sessionId: summary.sessionId, snippet: title })
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
434
|
+
if (typeof summary.cwd === 'string' && summary.cwd !== '' && summary.cwd.toLowerCase().includes(needle)) {
|
|
435
|
+
out.push({ sessionId: summary.sessionId, snippet: summary.cwd })
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return out
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** 先 search,失败就退回 list;返回 {hits, matchedBy} 让调用方看得见走了哪条路。 */
|
|
442
|
+
const sessionFind = async (query) => {
|
|
443
|
+
try {
|
|
444
|
+
return { hits: await sessionSearch(query), matchedBy: 'search' }
|
|
445
|
+
} catch (e) {
|
|
446
|
+
if (!looksLikeDisabledSearch(e)) throw e
|
|
447
|
+
return { hits: await sessionList(query), matchedBy: 'list' }
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
405
451
|
/** 只读解析:query → search 选一;sessionId → 直接用。任何不确定性都返回 reason。 */
|
|
406
452
|
const resolveSessionTarget = async (target) => {
|
|
407
|
-
|
|
453
|
+
const controller = sessionControllerNow()
|
|
454
|
+
if (controller === undefined) {
|
|
408
455
|
return { ok: false, reason: REASONS.CONTROLLER_UNAVAILABLE, message: 'sessionController service is not mounted in this profile', candidates: [] }
|
|
409
456
|
}
|
|
410
457
|
let hits = []
|
|
458
|
+
let matchedBy = 'sessionId'
|
|
411
459
|
if (target.sessionId === undefined) {
|
|
412
460
|
try {
|
|
413
|
-
|
|
461
|
+
const found = await sessionFind(target.query)
|
|
462
|
+
hits = found.hits
|
|
463
|
+
matchedBy = found.matchedBy
|
|
414
464
|
} catch (e) {
|
|
415
465
|
return { ok: false, reason: REASONS.GATEWAY_INTERNAL, message: errText(e), candidates: [] }
|
|
416
466
|
}
|
|
@@ -419,14 +469,14 @@ export async function apply(ctx) {
|
|
|
419
469
|
if (!picked.ok) return picked
|
|
420
470
|
let result
|
|
421
471
|
try {
|
|
422
|
-
result = await
|
|
472
|
+
result = await controller.resolveAgent(picked.sessionId)
|
|
423
473
|
} catch (e) {
|
|
424
474
|
return { ok: false, reason: mapControllerError(e), message: errText(e), candidates: picked.candidates }
|
|
425
475
|
}
|
|
426
476
|
if (result !== undefined && result !== null && result.error !== undefined) {
|
|
427
477
|
return { ok: false, reason: mapControllerError(result.error), message: errText(result.error), candidates: picked.candidates }
|
|
428
478
|
}
|
|
429
|
-
return { ok: true, sessionId: picked.sessionId, snippet: picked.snippet, candidates: picked.candidates }
|
|
479
|
+
return { ok: true, sessionId: picked.sessionId, snippet: picked.snippet, candidates: picked.candidates, matchedBy }
|
|
430
480
|
}
|
|
431
481
|
|
|
432
482
|
const sessionPendingTask = (sessionId) => tasks.find((t) => (t.status === 'accepted' || t.status === 'running') && t.target !== undefined && t.target.kind === 'session' && t.target.sessionId === sessionId)
|
|
@@ -478,7 +528,9 @@ export async function apply(ctx) {
|
|
|
478
528
|
record.watcher = 'follow'
|
|
479
529
|
void (async () => {
|
|
480
530
|
try {
|
|
481
|
-
const
|
|
531
|
+
const controllerRef = sessionControllerNow()
|
|
532
|
+
if (controllerRef === undefined) throw new Error('sessionController is not mounted')
|
|
533
|
+
const stream = controllerRef.follow({ address: { kind: 'session', sessionId: record.sessionId } }, controller.signal)
|
|
482
534
|
for await (const frame of stream) {
|
|
483
535
|
if (record.settled) break
|
|
484
536
|
if (frame === null || typeof frame !== 'object') continue
|
|
@@ -523,6 +575,7 @@ export async function apply(ctx) {
|
|
|
523
575
|
task.target.sessionId = resolved.sessionId
|
|
524
576
|
if (resolved.snippet !== undefined) task.target.snippet = resolved.snippet
|
|
525
577
|
if (resolved.candidates.length > 0) task.candidates = resolved.candidates
|
|
578
|
+
if (resolved.matchedBy !== undefined) task.matchedBy = resolved.matchedBy
|
|
526
579
|
|
|
527
580
|
const inFlight = sessionPendingTask(resolved.sessionId)
|
|
528
581
|
if (inFlight !== undefined && inFlight.taskId !== task.taskId) {
|
|
@@ -552,7 +605,17 @@ export async function apply(ctx) {
|
|
|
552
605
|
sessionTargets.set(resolved.sessionId, record)
|
|
553
606
|
|
|
554
607
|
try {
|
|
555
|
-
|
|
608
|
+
const controllerRef = sessionControllerNow()
|
|
609
|
+
if (controllerRef === undefined) {
|
|
610
|
+
sessionTargets.delete(resolved.sessionId)
|
|
611
|
+
record.settled = true
|
|
612
|
+
task.status = 'error'
|
|
613
|
+
task.error = 'sessionController service is not mounted in this profile'
|
|
614
|
+
task.reason = REASONS.CONTROLLER_UNAVAILABLE
|
|
615
|
+
pushTask(task.taskId, 'error', { error: task.error, reason: task.reason, target: { kind: 'session', sessionId: resolved.sessionId } })
|
|
616
|
+
return { task, failed: { reason: task.reason, message: task.error, candidates: [] } }
|
|
617
|
+
}
|
|
618
|
+
await controllerRef.prompt({
|
|
556
619
|
requestId: task.taskId,
|
|
557
620
|
sessionId: resolved.sessionId,
|
|
558
621
|
mode: deliver,
|
|
@@ -720,7 +783,8 @@ export async function apply(ctx) {
|
|
|
720
783
|
}
|
|
721
784
|
|
|
722
785
|
// 完成信号:会话从 running 回到 idle 即这一轮结束(follow 只负责流式进度)。
|
|
723
|
-
|
|
786
|
+
// 订阅的是 ctx 上的事件,与服务何时注册无关,所以不设门禁。
|
|
787
|
+
{
|
|
724
788
|
ctx.effect(() => {
|
|
725
789
|
const off = ctx.on('api-session/status', (sessionId, running) => {
|
|
726
790
|
const record = sessionTargets.get(sessionId)
|
|
@@ -819,12 +883,24 @@ export async function apply(ctx) {
|
|
|
819
883
|
} else {
|
|
820
884
|
parsed = qsOf(req)
|
|
821
885
|
}
|
|
822
|
-
|
|
823
|
-
|
|
886
|
+
const controller = sessionControllerNow()
|
|
887
|
+
if (controller === undefined) {
|
|
888
|
+
sendJson(res, 503, { error: 'sessionController service is not mounted yet (it registers asynchronously after startup; retry shortly)', reason: REASONS.CONTROLLER_UNAVAILABLE })
|
|
824
889
|
return
|
|
825
890
|
}
|
|
826
891
|
const query = typeof parsed.query === 'string' && parsed.query.trim() !== '' ? parsed.query.trim() : undefined
|
|
827
892
|
const sessionId = typeof parsed.sessionId === 'string' && parsed.sessionId.trim() !== '' ? parsed.sessionId.trim() : undefined
|
|
893
|
+
// create 是显式动作:为探针/一次性任务建一条空白会话,不去碰任何已有会话。
|
|
894
|
+
if (parsed.create === true) {
|
|
895
|
+
const cwd = typeof parsed.cwd === 'string' && parsed.cwd.trim() !== '' ? parsed.cwd.trim() : undefined
|
|
896
|
+
try {
|
|
897
|
+
const created = await controller.create(cwd === undefined ? {} : { cwd }, signalOf())
|
|
898
|
+
sendJson(res, 201, { sessionId: created !== undefined && created !== null ? created.sessionId : undefined, agentPreset: created === undefined || created === null ? undefined : created.agentPreset })
|
|
899
|
+
} catch (e) {
|
|
900
|
+
sendJson(res, 502, { error: errText(e), reason: mapControllerError(e) })
|
|
901
|
+
}
|
|
902
|
+
return
|
|
903
|
+
}
|
|
828
904
|
if (query === undefined && sessionId === undefined) {
|
|
829
905
|
sendJson(res, 400, { error: 'query or sessionId required', reason: REASONS.INVALID_TARGET })
|
|
830
906
|
return
|
|
@@ -840,7 +916,9 @@ export async function apply(ctx) {
|
|
|
840
916
|
sendJson(res, 200, { items: [{ sessionId: resolved.sessionId, ...(resolved.snippet === undefined ? {} : { snippet: resolved.snippet }), agentAvailable: true }], hasMore: false })
|
|
841
917
|
return
|
|
842
918
|
}
|
|
843
|
-
|
|
919
|
+
// search 失败就退回 list:有些部署把 session-query 索引配成 openAt "never"。
|
|
920
|
+
const found = await sessionFind(query)
|
|
921
|
+
const hits = found.hits
|
|
844
922
|
const items = hits.slice(0, limit).map((h) => ({
|
|
845
923
|
sessionId: h.sessionId,
|
|
846
924
|
snippet: typeof h.snippet === 'string' ? h.snippet.slice(0, 200) : '',
|
|
@@ -849,7 +927,7 @@ export async function apply(ctx) {
|
|
|
849
927
|
// 只对前 5 条探能力:逐条 stat 会很贵,这里保持有界。
|
|
850
928
|
for (const item of items.slice(0, 5)) {
|
|
851
929
|
try {
|
|
852
|
-
const r = await
|
|
930
|
+
const r = await controller.resolveAgent(item.sessionId)
|
|
853
931
|
const failed = r !== undefined && r !== null && r.error !== undefined
|
|
854
932
|
item.agentAvailable = !failed
|
|
855
933
|
if (failed) item.problem = mapControllerError(r.error)
|
|
@@ -859,7 +937,7 @@ export async function apply(ctx) {
|
|
|
859
937
|
}
|
|
860
938
|
}
|
|
861
939
|
}
|
|
862
|
-
sendJson(res, 200, { items, hasMore: hits.length > items.length, total: hits.length })
|
|
940
|
+
sendJson(res, 200, { items, hasMore: hits.length > items.length, total: hits.length, matchedBy: found.matchedBy })
|
|
863
941
|
} catch (e) {
|
|
864
942
|
sendJson(res, 502, { error: errText(e), reason: mapControllerError(e) })
|
|
865
943
|
}
|
|
@@ -914,7 +992,9 @@ export async function apply(ctx) {
|
|
|
914
992
|
}
|
|
915
993
|
let cancelled = false
|
|
916
994
|
try {
|
|
917
|
-
|
|
995
|
+
const controller = sessionControllerNow()
|
|
996
|
+
if (controller === undefined) throw new Error('sessionController service is not mounted in this profile')
|
|
997
|
+
await controller.cancel({ sessionId: task.target.sessionId }, signalOf())
|
|
918
998
|
cancelled = true
|
|
919
999
|
} catch (e) {
|
|
920
1000
|
console.error('[dsh-bridge] session cancel failed:', errText(e).slice(0, 200))
|
|
@@ -960,6 +1040,7 @@ export async function apply(ctx) {
|
|
|
960
1040
|
taskId: task.taskId, status: task.status, kind: task.kind, cwd: task.cwd, lane: task.lane,
|
|
961
1041
|
target: task.target, deliver: task.deliver, instruction: task.instruction.slice(0, 300),
|
|
962
1042
|
result: task.result, error: task.error, reason: task.reason, fallback: task.fallback,
|
|
1043
|
+
...(task.matchedBy === undefined ? {} : { matchedBy: task.matchedBy }),
|
|
963
1044
|
...(Array.isArray(task.candidates) && task.candidates.length > 0 ? { candidates: task.candidates } : {}),
|
|
964
1045
|
...(record === undefined ? {} : { watcher: record.watcher, sawRunning: record.sawRunning }),
|
|
965
1046
|
})
|
|
@@ -971,7 +1052,7 @@ export async function apply(ctx) {
|
|
|
971
1052
|
lanes: Array.from(buckets.keys()),
|
|
972
1053
|
reviewers: Array.from(reviewerBuckets.keys()),
|
|
973
1054
|
sessions: Array.from(sessionTargets.keys()),
|
|
974
|
-
sessionController:
|
|
1055
|
+
sessionController: sessionControllerNow() !== undefined,
|
|
975
1056
|
tasks: tasks.slice(-20).map((t) => ({ taskId: t.taskId, kind: t.kind, status: t.status, cwd: t.cwd, lane: t.lane, target: t.target, deliver: t.deliver })),
|
|
976
1057
|
})
|
|
977
1058
|
},
|
|
@@ -982,7 +1063,15 @@ export async function apply(ctx) {
|
|
|
982
1063
|
path: '/api/dsh-bridge/debug',
|
|
983
1064
|
handler: async (req, res) => {
|
|
984
1065
|
if (!isLoopback(req)) { res.writeHead(403); res.end('forbidden'); return }
|
|
985
|
-
|
|
1066
|
+
// services 是现场探测(不是 apply 时的快照):sessionController 可能晚于本插件注册,
|
|
1067
|
+
// 这份清单能在"接不上会话"时直接告诉我们到底缺哪个服务。
|
|
1068
|
+
const probe = (name) => { try { return ctx.get(name) !== undefined } catch (e) { return false } }
|
|
1069
|
+
const out = {
|
|
1070
|
+
pending: 0, historyCount: history.length, presets: [], providers: [], buckets: [], reviewers: [],
|
|
1071
|
+
sessionController: probe('sessionController'),
|
|
1072
|
+
services: Object.fromEntries(['sessionController', 'connection', 'sessionQuery', 'sessionPersistence', 'workspaceRegistry', 'agents', 'subagents', 'agentPresets', 'timer', 'llm'].map((n) => [n, probe(n)])),
|
|
1073
|
+
sessionTargets: [], models: knownModels, modelProvider, modelListError, workspaces: [],
|
|
1074
|
+
}
|
|
986
1075
|
out.pending = tasks.filter((t) => t.status === 'accepted' || t.status === 'running').length
|
|
987
1076
|
try { out.providers = subagents.list() } catch (e) { out.providers = ['list failed: ' + errText(e)] }
|
|
988
1077
|
if (agentPresets !== undefined) {
|
|
@@ -1124,5 +1213,5 @@ export async function apply(ctx) {
|
|
|
1124
1213
|
sessionTargets.clear()
|
|
1125
1214
|
})
|
|
1126
1215
|
|
|
1127
|
-
console.log('[dsh-bridge] persistent gateway ready (host composition): task(new|session) · sessions · review · tasks · cancel · status · debug · WS | sessionController=' + (
|
|
1216
|
+
console.log('[dsh-bridge] persistent gateway ready (host composition): task(new|session) · sessions · review · tasks · cancel · status · debug · WS | sessionController=' + (sessionControllerNow() !== undefined ? 'yes' : 'late-bound (registers asynchronously)'))
|
|
1128
1217
|
}
|
|
@@ -190,6 +190,34 @@ export const mapControllerError = (error) => {
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* 从 SessionSummary 里取标题。
|
|
195
|
+
*
|
|
196
|
+
* 标题不在 summary 顶层:它来自投影缓存(`projections.values.title`),而且不同 DSH
|
|
197
|
+
* 版本可能给字符串或 `{ val }` 包装,两种都认。`list` 在**不激活 Agent、也不打开
|
|
198
|
+
* search 索引**的前提下读持久化 header 与投影缓存,因此在 search 被部署禁用
|
|
199
|
+
* (session-query 索引 openAt: "never")时,它是唯一还能按标题找会话的路径。
|
|
200
|
+
*/
|
|
201
|
+
export const sessionTitleOf = (summary) => {
|
|
202
|
+
if (summary === null || typeof summary !== 'object') return ''
|
|
203
|
+
const projections = summary.projections
|
|
204
|
+
if (projections === null || typeof projections !== 'object') return ''
|
|
205
|
+
const values = projections.values
|
|
206
|
+
if (values === null || typeof values !== 'object') return ''
|
|
207
|
+
const title = values.title
|
|
208
|
+
if (typeof title === 'string') return title
|
|
209
|
+
if (title !== null && typeof title === 'object' && typeof title.val === 'string') return title.val
|
|
210
|
+
return ''
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** search 在本部署被禁用时的报错特征(session-query 索引 openAt: "never")。 */
|
|
214
|
+
export const looksLikeDisabledSearch = (error) => {
|
|
215
|
+
const text = error !== null && typeof error === 'object' && typeof error.message === 'string'
|
|
216
|
+
? error.message
|
|
217
|
+
: String(error === undefined || error === null ? '' : error)
|
|
218
|
+
return /search is disabled/i.test(text)
|
|
219
|
+
}
|
|
220
|
+
|
|
193
221
|
/** 任务在 HTTP 行上的回显:调用方一眼能看出这次是"新建"还是"接进哪个会话"。 */
|
|
194
222
|
export const taskResponse = (task) => {
|
|
195
223
|
const out = { taskId: task.taskId, status: task.status, kind: task.kind }
|
package/mcp/dsh-mcp.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// 客户端(Secure MCP Tunnel / cloudflared)桥接给 ChatGPT 等远程产品。
|
|
7
7
|
const GATEWAY = process.env.DSH_BRIDGE_URL || 'http://127.0.0.1:3080'
|
|
8
8
|
const SERVER_NAME = 'dsh-bridge'
|
|
9
|
-
const SERVER_VERSION = '0.1.
|
|
9
|
+
const SERVER_VERSION = '0.1.4'
|
|
10
10
|
const PROTOCOL_VERSION = '2024-11-05'
|
|
11
11
|
|
|
12
12
|
const log = (...args) => console.error('[dsh-mcp]', ...args)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whaletalk/dsh-codex-collab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Codex × DeepSeek Harness 双向编码协作:Codex 主导派活与验收,DeepSeek Harness 编码子代理在共享工作目录执行并回传,独立评审子代理实际跑通构建/测试后出具报告。提供 DSH 宿主网关插件、零依赖 MCP 服务器与 Codex 技能三件套。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|