clearai-dsh 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/LICENSE +201 -0
- package/README.md +138 -0
- package/README.zh-CN.md +138 -0
- package/bin/clearai.mjs +224 -0
- package/brand/README.md +41 -0
- package/brand/logo-512-dark.png +0 -0
- package/brand/logo-512.png +0 -0
- package/brand/logo-lockup-dark.png +0 -0
- package/brand/logo-lockup.png +0 -0
- package/brand/logo-lockup.svg +12 -0
- package/brand/logo-wordmark.svg +6 -0
- package/brand/logo.svg +19 -0
- package/cordis.patch.yml +39 -0
- package/lib/client.js +3071 -0
- package/lib/fold.js +1576 -0
- package/lib/host.js +605 -0
- package/package.json +65 -0
- package/presets/clearai/agent.cordis.yml +226 -0
- package/presets/clearai/plugins/brain.js +547 -0
- package/presets/clearai/plugins/clearai-kernel.js +5485 -0
- package/presets/clearai/plugins/ontology.js +306 -0
- package/presets/clearai/plugins/prompts.js +312 -0
- package/presets/clearai/preset.yml +5 -0
- package/presets/clearai/skills/clearai-loop/SKILL.md +89 -0
- package/presets/clearai/template/knowledge/README.md +25 -0
- package/presets/clearai/template/memory/README.md +34 -0
- package/presets/clearai/template/project.md +49 -0
- package/presets/clearai/template/skills/README.md +37 -0
- package/presets/clearai/template/skills/chart-diagram-qa/SKILL.md +43 -0
- package/presets/clearai/template/skills/citation-management/SKILL.md +73 -0
- package/presets/clearai/template/skills/citation-management/references/bibtex_formatting.md +908 -0
- package/presets/clearai/template/skills/citation-management/references/citation_validation.md +794 -0
- package/presets/clearai/template/skills/citation-management/references/google_scholar_search.md +725 -0
- package/presets/clearai/template/skills/citation-management/references/metadata_extraction.md +870 -0
- package/presets/clearai/template/skills/citation-management/references/pubmed_search.md +839 -0
- package/presets/clearai/template/skills/citation-management/scripts/doi_to_bibtex.py +204 -0
- package/presets/clearai/template/skills/citation-management/scripts/extract_metadata.py +569 -0
- package/presets/clearai/template/skills/citation-management/scripts/format_bibtex.py +349 -0
- package/presets/clearai/template/skills/citation-management/scripts/generate_schematic.py +139 -0
- package/presets/clearai/template/skills/citation-management/scripts/generate_schematic_ai.py +817 -0
- package/presets/clearai/template/skills/citation-management/scripts/search_google_scholar.py +282 -0
- package/presets/clearai/template/skills/citation-management/scripts/search_pubmed.py +398 -0
- package/presets/clearai/template/skills/citation-management/scripts/validate_citations.py +497 -0
- package/presets/clearai/template/skills/data-analysis/SKILL.md +92 -0
- package/presets/clearai/template/skills/data-analysis/checklists/readiness_check.md +23 -0
- package/presets/clearai/template/skills/data-analysis/templates/analysis_report.md.tpl +63 -0
- package/presets/clearai/template/skills/data-analysis/templates/cleaning_rules_draft.yaml.tpl +32 -0
- package/presets/clearai/template/skills/data-analysis/templates/data_dictionary.md.tpl +12 -0
- package/presets/clearai/template/skills/data-analysis/templates/domain_knowledge_template.md.tpl +316 -0
- package/presets/clearai/template/skills/data-analysis/templates/feature_candidates.json.tpl +20 -0
- package/presets/clearai/template/skills/data-analysis/templates/quality_scorecard.md.tpl +30 -0
- package/presets/clearai/template/skills/data-analysis/workflows/01-data-profiling.md +42 -0
- package/presets/clearai/template/skills/data-analysis/workflows/02-quality-audit.md +36 -0
- package/presets/clearai/template/skills/data-analysis/workflows/03-physical-correlation.md +25 -0
- package/presets/clearai/template/skills/data-analysis/workflows/04-unstructured-mining.md +26 -0
- package/presets/clearai/template/skills/data-qa-analysis/SKILL.md +102 -0
- package/presets/clearai/template/skills/data-qa-analysis/checklists/readiness_check.md +62 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/best_in_class_report.md.tpl +56 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/cleaning_rules_draft.yaml.tpl +56 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/data_dictionary.md.tpl +13 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/data_source_inventory_and_lineage.md.tpl +146 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/data_status_report.md.tpl +60 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/steady_state_rules.yaml.tpl +41 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/subsystem_registry.md.tpl +101 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/unified_execution_plan.md.tpl +100 -0
- package/presets/clearai/template/skills/data-qa-analysis/workflows/01-data-source-inventory-and-lineage.md +194 -0
- package/presets/clearai/template/skills/data-qa-analysis/workflows/02-data-alignment-and-tag-semantics.md +122 -0
- package/presets/clearai/template/skills/data-qa-analysis/workflows/03-steady-state-identification.md +126 -0
- package/presets/clearai/template/skills/data-qa-analysis/workflows/04-consumption-analysis.md +152 -0
- package/presets/clearai/template/skills/data-qa-analysis/workflows/05-best-in-class-and-optimization-space.md +78 -0
- package/presets/clearai/template/skills/domain-presearch/SKILL.md +131 -0
- package/presets/clearai/template/skills/domain-presearch/checklists/domain_checklist.md +24 -0
- package/presets/clearai/template/skills/domain-presearch/references/figure_code.md +78 -0
- package/presets/clearai/template/skills/domain-presearch/references/strategic_frameworks.md +38 -0
- package/presets/clearai/template/skills/exploration-loop/SKILL.md +81 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/SKILL.md +77 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/bioinformatics_genomics_formats.md +664 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/chemistry_molecular_formats.md +664 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/general_scientific_formats.md +518 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/microscopy_imaging_formats.md +620 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/proteomics_metabolomics_formats.md +517 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/spectroscopy_analytical_formats.md +633 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/scripts/eda_analyzer.py +547 -0
- package/presets/clearai/template/skills/hypothesis-generation/SKILL.md +73 -0
- package/presets/clearai/template/skills/hypothesis-generation/references/experimental_design_patterns.md +329 -0
- package/presets/clearai/template/skills/hypothesis-generation/references/hypothesis_quality_criteria.md +198 -0
- package/presets/clearai/template/skills/hypothesis-generation/references/literature_search_strategies.md +622 -0
- package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic.py +139 -0
- package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic_ai.py +817 -0
- package/presets/clearai/template/skills/literature-review/SKILL.md +72 -0
- package/presets/clearai/template/skills/literature-review/references/citation_styles.md +166 -0
- package/presets/clearai/template/skills/literature-review/references/database_strategies.md +455 -0
- package/presets/clearai/template/skills/literature-review/scripts/generate_pdf.py +176 -0
- package/presets/clearai/template/skills/literature-review/scripts/generate_schematic.py +139 -0
- package/presets/clearai/template/skills/literature-review/scripts/generate_schematic_ai.py +817 -0
- package/presets/clearai/template/skills/literature-review/scripts/search_databases.py +303 -0
- package/presets/clearai/template/skills/literature-review/scripts/verify_citations.py +221 -0
- package/presets/clearai/template/skills/paper-lookup/SKILL.md +59 -0
- package/presets/clearai/template/skills/paper-lookup/references/arxiv.md +161 -0
- package/presets/clearai/template/skills/paper-lookup/references/biorxiv.md +118 -0
- package/presets/clearai/template/skills/paper-lookup/references/core.md +150 -0
- package/presets/clearai/template/skills/paper-lookup/references/crossref.md +181 -0
- package/presets/clearai/template/skills/paper-lookup/references/medrxiv.md +104 -0
- package/presets/clearai/template/skills/paper-lookup/references/openalex.md +174 -0
- package/presets/clearai/template/skills/paper-lookup/references/pmc.md +152 -0
- package/presets/clearai/template/skills/paper-lookup/references/pubmed.md +124 -0
- package/presets/clearai/template/skills/paper-lookup/references/semantic-scholar.md +203 -0
- package/presets/clearai/template/skills/paper-lookup/references/unpaywall.md +127 -0
- package/presets/clearai/template/skills/process-presearch/SKILL.md +196 -0
- package/presets/clearai/template/skills/process-presearch/checklists/process_checklist.md +18 -0
- package/presets/clearai/template/skills/process-presearch/references/figure_code.md +107 -0
- package/presets/clearai/template/skills/process-presearch/references/source_attribution_example.md +22 -0
- package/presets/clearai/template/skills/process-understanding-extraction/SKILL.md +69 -0
- package/presets/clearai/template/skills/process-understanding-extraction/checklists/readiness_check.md +34 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/docx_raw_dump_extractor.py.tpl +132 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/entity_map_unit_topology.json.tpl +86 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief.md.tpl +89 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief_builder_from_raw_dump.py.tpl +203 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/process_flow_mermaid.md.tpl +41 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/unified_execution_plan.md.tpl +53 -0
- package/presets/clearai/template/skills/process-understanding-extraction/workflows/01-process-doc-discovery.md +173 -0
- package/presets/clearai/template/skills/process-understanding-extraction/workflows/02-process-understanding-and-diagramming.md +106 -0
- package/presets/clearai/template/skills/scientific-brainstorming/SKILL.md +64 -0
- package/presets/clearai/template/skills/scientific-brainstorming/references/brainstorming_methods.md +326 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/SKILL.md +72 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/common_biases.md +364 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/evidence_hierarchy.md +485 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/experimental_design.md +496 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/logical_fallacies.md +478 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/scientific_method.md +169 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/statistical_pitfalls.md +506 -0
- package/presets/clearai/template/skills/skill-creator/SKILL.md +109 -0
- package/presets/clearai/template/skills/skill-creator/references/authoring-guide.md +89 -0
- package/presets/clearai/template/skills/statistical-analysis/SKILL.md +79 -0
- package/presets/clearai/template/skills/statistical-analysis/references/assumptions_and_diagnostics.md +369 -0
- package/presets/clearai/template/skills/statistical-analysis/references/bayesian_statistics.md +653 -0
- package/presets/clearai/template/skills/statistical-analysis/references/effect_sizes_and_power.md +578 -0
- package/presets/clearai/template/skills/statistical-analysis/references/reporting_standards.md +469 -0
- package/presets/clearai/template/skills/statistical-analysis/references/test_selection_guide.md +129 -0
- package/presets/clearai/template/skills/statistical-analysis/scripts/assumption_checks.py +538 -0
- package/presets/clearai/template/skills/web-artifact/SKILL.md +165 -0
- package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.css +229 -0
- package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.js +373 -0
- package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/LICENSE +263 -0
- package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/UPSTREAM.md +26 -0
- package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/elk.bundled.js +6605 -0
- package/presets/clearai/template/skills/web-artifact/references/when-drawing-a-topology.md +150 -0
- package/presets/clearai/template/skills/web-artifact/references/when-the-page-must-work-offline.md +62 -0
- package/presets/clearai/template/skills/web-artifact/scripts/check_artifact.py +167 -0
- package/presets/clearai/template/skills/web-artifact/scripts/render_topology.js +272 -0
- package/presets/clearai/template/skills/what-if-oracle/LICENSE.txt +5 -0
- package/presets/clearai/template/skills/what-if-oracle/SKILL.md +72 -0
- package/presets/clearai/template/skills/what-if-oracle/references/scenario-templates.md +154 -0
package/lib/host.js
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* clearai-dsh —— 宿主半。
|
|
3
|
+
*
|
|
4
|
+
* 这一半只做两件事,都是**进程级、只做一次**的:
|
|
5
|
+
* ① 注册一个会话投影单元 `clearai`:ClearAI 的状态不是被存下来的,而是**会话日志的投影**。
|
|
6
|
+
* fold 是纯同步函数(见 ./fold.js),认的是工具写进 `tool/result.meta` 的变更记录。
|
|
7
|
+
* ② 发布服务 `clearai`,给预设侧(意图工具、运行态卡)读状态用——
|
|
8
|
+
* 与 DSH 自己的 `goals`(宿主服务 + 预设只放工具)同构。
|
|
9
|
+
*
|
|
10
|
+
* 为什么状态机必须在这一层:预设会被**重建**(组合文件的 mtime 一变,名册就重新挂载),
|
|
11
|
+
* 而进程级的东西(投影注册、服务名)只能注册一次。把状态放在预设里,等于让一个会被重建的
|
|
12
|
+
* 东西持有进程级资源——第二次挂载必然撞名(实测:webserver duplicate exact route)。
|
|
13
|
+
*
|
|
14
|
+
* 面板也不再需要 HTTP 路由:投影单元的 `wire` 把视图推给浏览器,客户端用
|
|
15
|
+
* `useProjection('clearai')` 读——免轮询、自带变更通知。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readdirSync, statSync } from 'node:fs'
|
|
19
|
+
import { join, relative, resolve, sep } from 'node:path'
|
|
20
|
+
import { z } from 'zod'
|
|
21
|
+
import { AUTONOMY_VALUES, HUMAN_GATE_ACTIONS, HUMAN_GATE_MARK, MUTATION_KIND, STATE_VERSION, applyEvent, applyMutations, derive, emptyState, renderCard, view } from './fold.js'
|
|
22
|
+
|
|
23
|
+
export const name = 'clearai-host'
|
|
24
|
+
/** 投影注册表与会话存储:两个都是宿主服务,这里只消费。 */
|
|
25
|
+
export const inject = ['sessionProjections', 'sessions']
|
|
26
|
+
|
|
27
|
+
/** 状态:纯 JSON(投影的持久化缓存前提)。这里只做形状校验,细节由 fold 保证。 */
|
|
28
|
+
const stateSchema = z.looseObject({
|
|
29
|
+
sessionId: z.string().optional(),
|
|
30
|
+
goal: z.unknown().nullable(),
|
|
31
|
+
hypotheses: z.array(z.unknown()),
|
|
32
|
+
plans: z.array(z.unknown()),
|
|
33
|
+
evidence: z.array(z.unknown()),
|
|
34
|
+
audits: z.array(z.unknown()),
|
|
35
|
+
materials: z.array(z.unknown()),
|
|
36
|
+
facts: z.array(z.unknown()),
|
|
37
|
+
forks: z.array(z.unknown()),
|
|
38
|
+
scouts: z.array(z.unknown()),
|
|
39
|
+
blocks: z.record(z.string(), z.unknown()),
|
|
40
|
+
releases: z.array(z.unknown()),
|
|
41
|
+
/** 宿主原生的合并技能目录(内核取一次快照,变了才发)。 */
|
|
42
|
+
skillCatalog: z.unknown().nullable(),
|
|
43
|
+
/** 本会话的技能加载记录(从 tool/call 与 `/名字` 用户消息折出来,零新账)。 */
|
|
44
|
+
skillUsage: z.record(z.string(), z.unknown()),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/** 浏览器读到的那个视图(面板契约,见 DESIGN.md §3.2)。 */
|
|
48
|
+
const viewSchema = z.looseObject({
|
|
49
|
+
ok: z.boolean(),
|
|
50
|
+
mounted: z.boolean(),
|
|
51
|
+
sessionId: z.string().nullable(),
|
|
52
|
+
goal: z.unknown().nullable(),
|
|
53
|
+
plan: z.unknown().nullable(),
|
|
54
|
+
/** 收件箱:面板「需要你 N」的数据面(条目只带分诊信息,见 fold.js 的 derive)。 */
|
|
55
|
+
inbox: z.array(z.unknown()),
|
|
56
|
+
hasOpenGate: z.boolean(),
|
|
57
|
+
/** 外脑全景:技能清单与记忆索引(内核扫描后随投影下发,面板据此画「技能 · 记忆」页签)。 */
|
|
58
|
+
brain: z.unknown().nullable(),
|
|
59
|
+
/** 技能面:**合并目录**(与模型同一张表)+ 本会话用量(面板「技能 · 记忆」页签)。 */
|
|
60
|
+
skills: z.unknown(),
|
|
61
|
+
evidence: z.array(z.unknown()),
|
|
62
|
+
audits: z.array(z.unknown()),
|
|
63
|
+
materials: z.array(z.unknown()),
|
|
64
|
+
facts: z.array(z.unknown()),
|
|
65
|
+
forks: z.array(z.unknown()),
|
|
66
|
+
scouts: z.array(z.unknown()),
|
|
67
|
+
settlement: z.array(z.unknown()),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export function apply(ctx) {
|
|
71
|
+
// 视图按状态引用记忆:同一份状态不重复造对象(投影用 Object.is 判断要不要发布)
|
|
72
|
+
let lastState = null
|
|
73
|
+
let lastView = null
|
|
74
|
+
const memoView = (state) => {
|
|
75
|
+
if (state === lastState && lastView !== null) return lastView
|
|
76
|
+
lastState = state
|
|
77
|
+
lastView = view(state)
|
|
78
|
+
return lastView
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ctx.effect(
|
|
82
|
+
() =>
|
|
83
|
+
ctx.sessionProjections.register({
|
|
84
|
+
key: 'clearai',
|
|
85
|
+
stateVersion: STATE_VERSION,
|
|
86
|
+
stateSchema,
|
|
87
|
+
init: (header) => ({ ...emptyState(), sessionId: String(header.id) }),
|
|
88
|
+
/**
|
|
89
|
+
* 纯同步 fold:见 fold.js 的 applyEvent——
|
|
90
|
+
* 它吃工具写进 `tool/result.meta` 的变更记录,也吃 `tool/call`
|
|
91
|
+
* 带来的两条过程事实(在飞、自写路径)。不认识的事件原样返回旧状态。
|
|
92
|
+
*/
|
|
93
|
+
apply(state, event) {
|
|
94
|
+
return applyEvent(state, event)
|
|
95
|
+
},
|
|
96
|
+
wire: { viewSchema, view: memoView },
|
|
97
|
+
}),
|
|
98
|
+
'clearai: session projection unit',
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
/** 读面:预设侧与运行态卡都走这里,不各自维护一份状态。 */
|
|
102
|
+
const stateOf = (sessionId) => {
|
|
103
|
+
const session = ctx.sessions.get(sessionId)
|
|
104
|
+
if (session === undefined) return emptyState()
|
|
105
|
+
return ctx.sessionProjections.stateOf(session, 'clearai') ?? emptyState()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* ═══ 人门通道(D2=B:面板可写,但只写人门动作)═══
|
|
110
|
+
*
|
|
111
|
+
* 三条硬约束,每一条都有测试盯着:
|
|
112
|
+
* ① **动词白名单**:adopt_branch / abandon_fork / promote_skill,
|
|
113
|
+
* 表外一律拒(与贡献表同一套「表外的名字不许出现」)——**取值**也在这一层校验
|
|
114
|
+
* (技能名合原生语法),宁可 400,也不静默半生效;
|
|
115
|
+
* `set_autonomy` 于 §34 摘掉:「在场与否」是运行时状态,不该是面板上的一个开关 ✗;
|
|
116
|
+
* ② **agent 不可达**:这些动词**没有工具 schema**——模型的工具面里不存在它们。
|
|
117
|
+
* 模型能做的只是读到「人做了什么」这条事实(它进的是会话日志,不是内核状态);
|
|
118
|
+
* ③ **留下署名**:动作变成一条 `source.kind === 'user'` 的消息,折进投影时写
|
|
119
|
+
* `by:'user'`——「谁在什么时候裁了哪条世界线、扶正了哪个技能、把档切成了什么」都在日志里。
|
|
120
|
+
*
|
|
121
|
+
* 2026-09-11 砍掉两个动词(奥卡姆:它们是重复,不是能力):
|
|
122
|
+
* · `confirm_plan` —— 原生 `dsh-plan-mode` 就是「用户复核的出口」;而授权本来就「交付即落账」;
|
|
123
|
+
* · `invoke_skill` —— 原生 `/` 技能触发器做同一件事(还带候选菜单),我们那条只是在旁边
|
|
124
|
+
* 又写了一遍同一个手势。
|
|
125
|
+
*
|
|
126
|
+
* 为什么走 HTTP 路由而不是 `harness.handle`:那是 cordis **动态插件**的座位,静态客户端包
|
|
127
|
+
* 没有它(实测:全仓 `host.call` 只出现在动态运行时的两个包里)。宿主平面的路由是这条
|
|
128
|
+
* 通道在静态包里的对应物,与 ClearAI 面板自己的写接口(FastAPI 路由)同一个层级。
|
|
129
|
+
*/
|
|
130
|
+
/**
|
|
131
|
+
* 面板用的三条 HTTP 面**必须挂 `connection` 的 exact fetch route 表**,不能只挂 `webServer`
|
|
132
|
+
* (2026-09-11 实测换来的教训)。
|
|
133
|
+
*
|
|
134
|
+
* 浏览器那侧的 `/api/*` 先落到 connection 的共享 channel:它先查 exact fetch route 表;
|
|
135
|
+
* 查不到就把路径当成 RPC 端点(`/api/<ns>.<method>`)去问 interceptor;再查不到,
|
|
136
|
+
* **直接 `404 "not found"`**。而 `webServer.register({kind:'exact'})` 挂在 `/api` 前缀那条
|
|
137
|
+
* **后面**,浏览器永远轮不到——面板上的表现就是「点引用 → Unexpected token 'o',
|
|
138
|
+
* "not found" is not valid JSON」「点技能进不去预览」「产物读不到」。
|
|
139
|
+
* 原生包也是挂在 fetch 表上的(见 `dsh-client-ui-deliverables` 的 `/api/present.open`)。
|
|
140
|
+
*
|
|
141
|
+
* 另一件事:connection 在**没有 web 的档**(headless / SDK)里不存在,所以这里用
|
|
142
|
+
* `ctx.inject([...])` **延迟注册**——服务出现才挂、消失就收。原来那种「apply 里
|
|
143
|
+
* `ctx.get('webServer')` 拿一次、拿不到就整段跳过」的写法,在服务晚到时是**静默不挂**。
|
|
144
|
+
*/
|
|
145
|
+
const reply = (status, payload) => Response.json(payload, { status, headers: { 'cache-control': 'no-store' } })
|
|
146
|
+
/**
|
|
147
|
+
* 产物的**语义分类**:
|
|
148
|
+
* 章程 `PROJECT.md` / 项目资料 `input/` / 分析过程 `lab/` / 输出成果 `products/` / 外脑 `clear/`。
|
|
149
|
+
* 面板据此把「核心产物」排在前面,而不是把 100 个中间文件与 3 份交付物混在一起。
|
|
150
|
+
*/
|
|
151
|
+
const pathAreaOf = (path) => {
|
|
152
|
+
const clean = String(path ?? '').replace(/^\.\//, '')
|
|
153
|
+
if (clean === 'PROJECT.md' || clean === 'project.md') return { key: 'constitution', label: '章程' }
|
|
154
|
+
if (clean.startsWith('input/')) return { key: 'input', label: '项目资料' }
|
|
155
|
+
if (clean.startsWith('lab/')) return { key: 'process', label: '分析过程' }
|
|
156
|
+
if (clean.startsWith('products/')) return { key: 'output', label: '输出成果' }
|
|
157
|
+
if (clean.startsWith('clear/')) return { key: 'brain', label: '外脑' }
|
|
158
|
+
return { key: 'other', label: '未分类' }
|
|
159
|
+
}
|
|
160
|
+
const resolveInside = (cwd, candidate) => {
|
|
161
|
+
if (typeof candidate !== 'string' || candidate === '') return null
|
|
162
|
+
const base = resolve(cwd)
|
|
163
|
+
const target = resolve(base, candidate)
|
|
164
|
+
if (target !== base && !target.startsWith(`${base}${sep}`)) return null
|
|
165
|
+
return target
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 盘上**实际**的输出成果:`products/` 底下真有、但**任何计划都还没声明**的那些文件。
|
|
169
|
+
*
|
|
170
|
+
* 为什么要它(2026-09-11 用户实测):在一个已经有产物的老项目里开新会话,「产物」栏只有
|
|
171
|
+
* 「阶段 0 · 交付 0/0」—— 因为这一栏原来只认投影里的**声明**。可人此刻问的是「我拿到了什么」,
|
|
172
|
+
* 盘上那份 46 KB 的 HTML 明明就在。声明与实际是两件事,这一栏本来就该两边都看得见。
|
|
173
|
+
*
|
|
174
|
+
* 奥卡姆:不新开读面、不建第二本账 —— 就是一次有界的目录遍历,字段与声明的产物**同一形状**,
|
|
175
|
+
* 所以面板能复用同一行渲染。边界写死在这里(depth ≤ 5、扫 ≤2000 条、返回 ≤200 条、
|
|
176
|
+
* 跳隐藏项与符号链接目录),因为这一栏是给人「第一眼」看的,把整个工作区倒进来就等于
|
|
177
|
+
* 把它变回一个文件管理器。
|
|
178
|
+
*
|
|
179
|
+
* 两个上限分工不同:**扫描上限**防的是巨大的 products/(扫描要停得下来),
|
|
180
|
+
* **返回上限**防的是把 2000 条全塞给面板。上限留在结果侧而不是扫描侧,返回的那 200 条
|
|
181
|
+
* 才真是**最近改动的**那 200 条,而不是 readdir 碰巧先遇到的那批。
|
|
182
|
+
*/
|
|
183
|
+
const OUTPUT_MAX_DEPTH = 5
|
|
184
|
+
const OUTPUT_MAX_SCAN = 2000
|
|
185
|
+
const OUTPUT_MAX_FILES = 200
|
|
186
|
+
const listWorkspaceOutputs = (cwd) => {
|
|
187
|
+
const root = resolveInside(cwd, 'products')
|
|
188
|
+
if (root === null) return []
|
|
189
|
+
const workspaceRoot = resolve(cwd)
|
|
190
|
+
const found = []
|
|
191
|
+
let scanned = 0
|
|
192
|
+
const walk = (dir, depth) => {
|
|
193
|
+
if (depth > OUTPUT_MAX_DEPTH || scanned >= OUTPUT_MAX_SCAN) return
|
|
194
|
+
let entries = []
|
|
195
|
+
try {
|
|
196
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
197
|
+
} catch {
|
|
198
|
+
// 目录读不了(权限/竞态):跳过这一支,不把整条读面拖成失败。
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
if (scanned >= OUTPUT_MAX_SCAN) return
|
|
203
|
+
if (entry.name.startsWith('.')) continue
|
|
204
|
+
const absolute = join(dir, entry.name)
|
|
205
|
+
// 符号链接不进(目录可能成环;文件也不是「盘上真的有一份」)。
|
|
206
|
+
if (entry.isDirectory()) {
|
|
207
|
+
walk(absolute, depth + 1)
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
if (!entry.isFile()) continue
|
|
211
|
+
scanned += 1
|
|
212
|
+
let info = null
|
|
213
|
+
try {
|
|
214
|
+
info = statSync(absolute)
|
|
215
|
+
} catch {
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
// 路径一律**相对工作区**(与声明的产物同一形式):面板把它原样交给原生预览,
|
|
219
|
+
// 原生按工作区根解析 —— 给成相对 products/ 的路径会指到别的文件上去。
|
|
220
|
+
found.push({ relative: relative(workspaceRoot, absolute), bytes: info.size, modifiedAt: info.mtimeMs })
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
walk(root, 1)
|
|
224
|
+
// 最近动过的排前面:人问的是「我拿到了什么」,新东西更可能是答案。
|
|
225
|
+
return found.sort((left, right) => right.modifiedAt - left.modifiedAt).slice(0, OUTPUT_MAX_FILES)
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* 一条人门消息:**署名是人 + 结构化标记 + 一句人话**。
|
|
229
|
+
* 两条路(面板直接点 / 原生提问卡答)共用它 ⇒ 无论从哪儿来,落进日志的是同一种事实。
|
|
230
|
+
*/
|
|
231
|
+
function appendHumanGate(agent, { detail, human, followUp }) {
|
|
232
|
+
const text = `${HUMAN_GATE_MARK} ${JSON.stringify(detail)}\n${human}这是**结构化的决定,不是商量**:${followUp}。\n`
|
|
233
|
+
const message = {
|
|
234
|
+
id: `clearai-gate-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
235
|
+
role: 'user',
|
|
236
|
+
content: [{ type: 'text', text }],
|
|
237
|
+
source: { kind: 'user' },
|
|
238
|
+
}
|
|
239
|
+
// 跑着就插到最近的步边界(steer);闲着就叫醒它(followup)。
|
|
240
|
+
if (agent.status === 'running') agent.steer(message)
|
|
241
|
+
else agent.followup(message)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 把一道开着的人门**建成一张原生提问卡**(§20 混合路径)。
|
|
246
|
+
*
|
|
247
|
+
* 问题与选项都从**投影**现算(此刻真实的分叉与技能),不在前端拼——前端只发一个手势。
|
|
248
|
+
* 认不出来(那道门已经不在了)就返回 `null`:如实拒掉,不摆一张过期的卡。
|
|
249
|
+
*/
|
|
250
|
+
function buildGateQuestion(gate, { sessionId, fork, skill }) {
|
|
251
|
+
const state = stateOf(sessionId)
|
|
252
|
+
const projected = view(state)
|
|
253
|
+
if (gate === 'fork_adopt') {
|
|
254
|
+
const target = (projected.forks ?? []).find((item) => item.id === fork && item.phase === 'deciding')
|
|
255
|
+
if (target === undefined) return null
|
|
256
|
+
const branches = target.branches ?? []
|
|
257
|
+
if (branches.length === 0) return null
|
|
258
|
+
const recommended = branches.find((branch) => branch.id === target.recommended) ?? null
|
|
259
|
+
const lines = [
|
|
260
|
+
`**要裁决的**:${target.question}`,
|
|
261
|
+
`**判据**:${target.decideBy?.metric ?? '—'}(取${target.decideBy?.direction === 'min' ? '最小' : '最大'})`,
|
|
262
|
+
'',
|
|
263
|
+
...branches.map((branch) => `- **${branch.label}**(${branch.id}${branch.id === target.recommended ? ' · 推荐' : ''}):${branch.approach} · 读数 ${branch.reading ?? '—'} · 有效性 ${branch.validity ?? '—'}`),
|
|
264
|
+
]
|
|
265
|
+
if (target.provisional === true) lines.push('', '⚠️ 这次裁决还是**暂定**的:证据不够硬。')
|
|
266
|
+
return {
|
|
267
|
+
kind: 'adopt_branch',
|
|
268
|
+
questions: [
|
|
269
|
+
{
|
|
270
|
+
id: 'fork-adopt',
|
|
271
|
+
header: '世界线裁决',
|
|
272
|
+
question: `采纳哪条世界线?(推荐:${recommended?.label ?? '无'})`,
|
|
273
|
+
detail: lines.join('\n'),
|
|
274
|
+
options: branches.map((branch) => ({
|
|
275
|
+
label: branch.label,
|
|
276
|
+
description: `${branch.approach} · 读数 ${branch.reading ?? '—'}`,
|
|
277
|
+
})),
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
byLabel: new Map(branches.map((branch) => [branch.label, branch.id])),
|
|
281
|
+
fork: target.id,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (gate === 'promote_skill') {
|
|
285
|
+
const candidate = (projected.brain?.skills ?? []).find((item) => item.name === skill && item.status === 'candidate')
|
|
286
|
+
if (candidate === undefined) return null
|
|
287
|
+
return {
|
|
288
|
+
kind: 'promote_skill',
|
|
289
|
+
questions: [
|
|
290
|
+
{
|
|
291
|
+
id: 'promote-skill',
|
|
292
|
+
header: '候选技能',
|
|
293
|
+
question: `采纳候选技能「${candidate.name}」?`,
|
|
294
|
+
detail: [`**它是什么**:${candidate.description ?? '(没有描述)'}`, `**落在哪**:${candidate.path ?? '—'}`, '', '采纳之后它会进入你的技能目录(内核改写 frontmatter),模型从此加载得到它。'].join('\n'),
|
|
295
|
+
options: [
|
|
296
|
+
{ label: '采纳', description: '写进技能目录,模型从此加载得到它。' },
|
|
297
|
+
{ label: '先不采纳', description: '候选留在工作区里,你以后还可以采纳。' },
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
byLabel: new Map([['采纳', 'yes'], ['先不采纳', 'no']]),
|
|
302
|
+
skill: candidate.name,
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 把原生卡上的答案翻译成**人门动作**;人没作答(撤下卡/跳过)⇒ `null`,什么都不落。
|
|
310
|
+
* 判据用的是我们自己的标签(与内核的 `intent.approve` 同一条纪律:按标签判,不按按钮文字)。
|
|
311
|
+
*/
|
|
312
|
+
function answerOf(asked, answer) {
|
|
313
|
+
const item = (Array.isArray(answer?.answers) ? answer.answers : []).find((entry) => entry?.id === asked.questions[0].id)
|
|
314
|
+
const selected = Array.isArray(item?.selected) ? item.selected : []
|
|
315
|
+
const custom = typeof item?.custom === 'string' ? item.custom.trim() : ''
|
|
316
|
+
if (selected.length === 0 && custom === '') return null
|
|
317
|
+
if (asked.kind === 'adopt_branch') {
|
|
318
|
+
// 人可以直接选一条,也可以在自定义框里打一条世界线 id 或名字。
|
|
319
|
+
const branchId = selected.length === 1 ? (asked.byLabel.get(selected[0]) ?? null) : (asked.byLabel.get(custom) ?? (/^b-[a-z0-9]+$/.test(custom) ? custom : null))
|
|
320
|
+
if (branchId === null) return null
|
|
321
|
+
return {
|
|
322
|
+
detail: { action: 'adopt_branch', plan: null, fork: asked.fork, branch: branchId, skill: null, value: null, note: null },
|
|
323
|
+
human: `人在原生提问卡上裁决:采纳这条世界线(${branchId})。`,
|
|
324
|
+
followUp: '跑 ConvergeFork 落实它——合并与清理是内核的活',
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (asked.kind === 'promote_skill') {
|
|
328
|
+
const choice = selected.length === 1 ? (asked.byLabel.get(selected[0]) ?? null) : (asked.byLabel.get(custom) ?? null)
|
|
329
|
+
if (choice !== 'yes') return null // 「先不采纳」= 人不采纳:什么都不落(与直接路径一致:不采纳不是一条事实)
|
|
330
|
+
return {
|
|
331
|
+
detail: { action: 'promote_skill', plan: null, fork: null, branch: null, skill: asked.skill, value: null, note: null },
|
|
332
|
+
human: `人在原生提问卡上采纳了候选技能「${asked.skill}」——它从此进你的技能目录。`,
|
|
333
|
+
followUp: '候选状态正由内核改写(`status: active`),下一个回合它就在你的技能目录里',
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return null
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
ctx.inject(['connection'], (connectionCtx) => {
|
|
340
|
+
/** 挂一条 exact fetch route:归属当前 fiber,connection 消失时自动收掉。 */
|
|
341
|
+
const route = (path, methods, fetch) =>
|
|
342
|
+
connectionCtx.effect(
|
|
343
|
+
() => connectionCtx.connection.fetch.register({ path, methods, requestBody: 'buffered', fetch: (request) => Promise.resolve(fetch(request)) }),
|
|
344
|
+
`clearai: ${path}`,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
route('/api/clearai/gate', ['POST'], async (httpRequest) => {
|
|
348
|
+
let request = null
|
|
349
|
+
try {
|
|
350
|
+
request = await httpRequest.json()
|
|
351
|
+
} catch {
|
|
352
|
+
// 连 JSON 都不是:如实说,而不是当成「未知动词」。
|
|
353
|
+
return reply(400, { ok: false, error: 'bad_json' })
|
|
354
|
+
}
|
|
355
|
+
const action = request === null || typeof request !== 'object' ? '' : String(request.action ?? '')
|
|
356
|
+
/**
|
|
357
|
+
* `ask` 是**界面手势**,不是事实动词(§20):它把某道等人的人门摆到**原生提问卡**上,
|
|
358
|
+
* 再由人在卡里选——选出来的答案仍走下面那条老路(人门消息 → 内核落事实)。
|
|
359
|
+
* 所以白名单分两层:能**写事实**的只有 `HUMAN_GATE_ACTIONS`;能**发起提问**的另有一个。
|
|
360
|
+
*/
|
|
361
|
+
const ASKABLE_GATES = ['fork_adopt', 'promote_skill']
|
|
362
|
+
if (action !== 'ask' && !HUMAN_GATE_ACTIONS.includes(action)) return reply(400, { ok: false, error: 'unknown_gate_action' })
|
|
363
|
+
const sessionId = request === null || typeof request !== 'object' ? '' : String(request.sessionId ?? '')
|
|
364
|
+
const agents = ctx.get('agents')
|
|
365
|
+
const agent = sessionId === '' ? undefined : agents?.get?.(sessionId)
|
|
366
|
+
if (agent === undefined) return reply(404, { ok: false, error: 'no_live_session' })
|
|
367
|
+
// 只挑需要的最小标量,不在 RPC 边界上搬活的会话对象。
|
|
368
|
+
const detail = {
|
|
369
|
+
action,
|
|
370
|
+
plan: typeof request.plan === 'string' ? request.plan : null,
|
|
371
|
+
fork: typeof request.fork === 'string' ? request.fork : null,
|
|
372
|
+
branch: typeof request.branch === 'string' ? request.branch : null,
|
|
373
|
+
skill: typeof request.skill === 'string' ? request.skill : null,
|
|
374
|
+
value: typeof request.value === 'string' ? request.value : null,
|
|
375
|
+
note: typeof request.note === 'string' ? request.note.slice(0, 200) : null,
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* 技能名与运行档都要先过**取值校验**,再进日志。
|
|
379
|
+
*
|
|
380
|
+
* 技能名必须是原生那条语法(kebab-case):它曾要变成 `/<名字>` 手势(原生 pre-step
|
|
381
|
+
* 认的就是 `[a-z0-9]+(-[a-z0-9]+)*`),放别的形状进去只会静默不生效——比拒绝更坏。
|
|
382
|
+
* 运行档只认两档;写别的值进去会让「立约即授权」这条机制在半个会话里失效,
|
|
383
|
+
* 而人以为切好了。表外的值一律拒,与动词白名单同一套纪律。
|
|
384
|
+
*/
|
|
385
|
+
if (action === 'promote_skill' && (detail.skill === null || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(detail.skill))) {
|
|
386
|
+
return reply(400, { ok: false, error: 'bad_skill_name' })
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* ── 混合路径(§20):点我们那条 → 用**原生提问卡**问 ──────────────────────
|
|
390
|
+
*
|
|
391
|
+
* 为什么值这一条:我们自己的人门行(fork 裁决 / 采纳技能)现在只能在右栏面板里点按钮,
|
|
392
|
+
* 而「人正在看对话」的时候,卡就在他眼前、还把输入框挡住——那是原生最擅长的形态。
|
|
393
|
+
* 借的只是**问答界面**:答案回来之后,落账的那条事实与直接点按钮**一模一样**
|
|
394
|
+
* (同一个人门消息、同一套校验、同一本账)。借界面,不借账。
|
|
395
|
+
*/
|
|
396
|
+
if (action === 'ask') {
|
|
397
|
+
const gate = String(request.gate ?? '')
|
|
398
|
+
if (!ASKABLE_GATES.includes(gate)) return reply(400, { ok: false, error: 'bad_gate' })
|
|
399
|
+
const questions = ctx.get('userQuestions')
|
|
400
|
+
if (questions === undefined || typeof questions.ask !== 'function') {
|
|
401
|
+
return reply(409, { ok: false, error: 'no_question_channel' })
|
|
402
|
+
}
|
|
403
|
+
const asked = buildGateQuestion(gate, { sessionId, fork: typeof request.fork === 'string' ? request.fork : null, skill: typeof request.skill === 'string' ? request.skill : null })
|
|
404
|
+
if (asked === null) return reply(409, { ok: false, error: 'gate_not_open' })
|
|
405
|
+
/**
|
|
406
|
+
* **不占着 HTTP 请求等人**:点击只是「把卡摆上去」,人什么时候答都行。
|
|
407
|
+
* 答案到了再落账(与直接点按钮同一条路),失败也只记一行日志——界面手势不该 500。
|
|
408
|
+
*/
|
|
409
|
+
void questions
|
|
410
|
+
.ask({ questions: asked.questions, agent })
|
|
411
|
+
.then((answer) => {
|
|
412
|
+
const chosen = answerOf(asked, answer)
|
|
413
|
+
if (chosen === null) return null // 人撤下了卡:什么都不落(他不算答过)
|
|
414
|
+
return appendHumanGate(agent, chosen)
|
|
415
|
+
})
|
|
416
|
+
.catch((error) => {
|
|
417
|
+
ctx.logger?.warn?.(`clearai gate: 原生提问失败 ${String(error?.message ?? error).slice(0, 160)}`)
|
|
418
|
+
})
|
|
419
|
+
return reply(200, { ok: true, asked: true, gate })
|
|
420
|
+
}
|
|
421
|
+
const human =
|
|
422
|
+
action === 'adopt_branch'
|
|
423
|
+
? `人在面板上裁决:采纳这条世界线(${detail.branch ?? '?'})。`
|
|
424
|
+
: action === 'promote_skill'
|
|
425
|
+
? `人在面板上采纳了候选技能「${detail.skill ?? '?'}」——它从此进你的技能目录。`
|
|
426
|
+
: '人在面板上做了一个动作。'
|
|
427
|
+
const followUp =
|
|
428
|
+
action === 'promote_skill'
|
|
429
|
+
? '候选状态正由内核改写(`status: active`),下一个回合它就在你的技能目录里'
|
|
430
|
+
/* 说准(rather than 说满):这句话写下的那一刻只到 inbox、还没进投影,
|
|
431
|
+
所以不能断言「已经在投影里生效」——实测里模型看到它与卡片矛盾,停下来问过人(§17.5)。 */
|
|
432
|
+
: '跑 ConvergeFork 落实它——合并与清理是内核的活'
|
|
433
|
+
try {
|
|
434
|
+
appendHumanGate(agent, { detail, human, followUp })
|
|
435
|
+
} catch (error) {
|
|
436
|
+
return reply(500, { ok: false, error: String(error?.message ?? error).slice(0, 200) })
|
|
437
|
+
}
|
|
438
|
+
return reply(200, { ok: true, action, sessionId })
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* ═══ 两条只读读面(面板在浏览器里读不了盘)═══
|
|
443
|
+
*
|
|
444
|
+
* `GET /api/clearai/deliverables?sessionId=` 交付物:按阶段(Plan)分组,每条给出
|
|
445
|
+
* 声明(步骤 artifacts)、盘上在不在、多大、谁验的。**声明 vs 实际**是这个产品的核心对照。
|
|
446
|
+
* `GET /api/clearai/brain?sessionId=` 工作区现状:技能目录(内核还没落事实时面板的兜底)。
|
|
447
|
+
*
|
|
448
|
+
* **没有「读文件正文」那条了**(2026-09-11 删):预览交给 DSH 原生的文档预览
|
|
449
|
+
* (`dsh-client-ui-sidebar-documentpreview` 的 6 个实现 + 它自己的分页读盘),
|
|
450
|
+
* 我们只按原生地址把文件递过去。少一条读面 = 少一处要维护的路径守卫,
|
|
451
|
+
* 而且原生那边本来就有 markdown / 图片 / pdf / html 的渲染。
|
|
452
|
+
*/
|
|
453
|
+
route('/api/clearai/deliverables', ['GET'], (httpRequest) => {
|
|
454
|
+
const url = new URL(httpRequest.url)
|
|
455
|
+
const session = ctx.sessions.get(url.searchParams.get('sessionId') ?? '')
|
|
456
|
+
const cwd = session?.header?.cwd
|
|
457
|
+
if (typeof cwd !== 'string' || cwd === '') return reply(404, { ok: false, error: 'no_live_session' })
|
|
458
|
+
// 只挑叶子字段构造属于我们自己的 JSON:不把投影对象整体搬出去。
|
|
459
|
+
const state = ctx.sessionProjections.stateOf(session, 'clearai')
|
|
460
|
+
const stages = (state?.plans ?? []).map((plan) => ({
|
|
461
|
+
plan: plan.id,
|
|
462
|
+
status: plan.status,
|
|
463
|
+
brief: plan.brief ?? '',
|
|
464
|
+
confirmedAt: plan.confirmed_at ?? null,
|
|
465
|
+
openedAt: plan.at ?? null,
|
|
466
|
+
closedAt: plan.closedAt ?? null,
|
|
467
|
+
summary: plan.summary ?? null,
|
|
468
|
+
steps: (plan.steps ?? []).map((step) => ({
|
|
469
|
+
id: step.id,
|
|
470
|
+
ordinal: step.ordinal,
|
|
471
|
+
do: step.do,
|
|
472
|
+
status: step.status,
|
|
473
|
+
doneCriteria: step.done_criteria,
|
|
474
|
+
level: step.tests?.level ?? null,
|
|
475
|
+
advancedAt: step.advancedAt ?? null,
|
|
476
|
+
voidReason: step.voidReason ?? null,
|
|
477
|
+
artifacts: (step.artifacts ?? []).map((artifact) => {
|
|
478
|
+
const absolute = resolveInside(cwd, artifact)
|
|
479
|
+
let info = null
|
|
480
|
+
try {
|
|
481
|
+
info = absolute === null ? null : statSync(absolute)
|
|
482
|
+
} catch {
|
|
483
|
+
info = null
|
|
484
|
+
}
|
|
485
|
+
const isFile = info !== null && info.isFile()
|
|
486
|
+
/**
|
|
487
|
+
* `exists: false` 有两种成因,**面板必须分开说**(2026-09-11 长测抓到的):
|
|
488
|
+
* 模型把 `lab/`(一个目录)声明成物证时,准入按「空目录」拒了它;
|
|
489
|
+
* 而面板原来一律写「缺 · 盘上没有这个文件」——它明明在,只是不是文件。
|
|
490
|
+
* 事实有两种,界面就别用一句话盖住。
|
|
491
|
+
*/
|
|
492
|
+
return {
|
|
493
|
+
path: artifact,
|
|
494
|
+
area: pathAreaOf(artifact),
|
|
495
|
+
exists: isFile,
|
|
496
|
+
directory: info !== null && info.isDirectory(),
|
|
497
|
+
bytes: isFile ? info.size : null,
|
|
498
|
+
modifiedAt: isFile ? info.mtimeMs : null,
|
|
499
|
+
}
|
|
500
|
+
}),
|
|
501
|
+
})),
|
|
502
|
+
}))
|
|
503
|
+
/**
|
|
504
|
+
* 「实际」那一半:盘上真有、但没有任何计划声明过的 products/ 文件。
|
|
505
|
+
* 与声明过的按**路径**去重 —— 同一条不许在栏里出现两遍。
|
|
506
|
+
*/
|
|
507
|
+
const declared = new Set(stages.flatMap((stage) => stage.steps.flatMap((step) => step.artifacts.map((artifact) => artifact.path))))
|
|
508
|
+
const outputs = listWorkspaceOutputs(cwd)
|
|
509
|
+
.filter((item) => !declared.has(item.relative))
|
|
510
|
+
.map((item) => ({ path: item.relative, area: pathAreaOf(item.relative), exists: true, bytes: item.bytes, modifiedAt: item.modifiedAt, declared: false }))
|
|
511
|
+
return reply(200, { ok: true, stages, outputs })
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* `GET /api/clearai/brain?sessionId=` —— 工作区**现状**的实时技能目录。
|
|
516
|
+
*
|
|
517
|
+
* 为什么需要它(2026-09-11 用户实测):面板的数据来自会话日志的投影,而内核只在
|
|
518
|
+
* `agent/pre-step` 里落事实;于是**新建的会话在第一轮对话之前**,「技能 · 记忆」页签是空的
|
|
519
|
+
* (提示还让人「先在 clear/skills/ 里放一个技能」——可那里明明躺着 18 个模板技能)。
|
|
520
|
+
*
|
|
521
|
+
* 这条读面直接问宿主的 `skills` 服务(它就是模型看到的那张合并目录),返回字段与内核
|
|
522
|
+
* 随投影下发的 `catalog.entries` **逐字段一致**,所以面板能用同一套渲染。
|
|
523
|
+
* 它是**读面不是事实**:面板会如实标注「本会话还没有第一轮对话,这份还没进投影」。
|
|
524
|
+
*/
|
|
525
|
+
route('/api/clearai/brain', ['GET'], async (httpRequest) => {
|
|
526
|
+
const url = new URL(httpRequest.url)
|
|
527
|
+
const session = ctx.sessions.get(url.searchParams.get('sessionId') ?? '')
|
|
528
|
+
const cwd = session?.header?.cwd
|
|
529
|
+
if (typeof cwd !== 'string' || cwd === '') return reply(404, { ok: false, error: 'no_live_session' })
|
|
530
|
+
const skills = ctx.get('skills')
|
|
531
|
+
if (skills === undefined || typeof skills.snapshot !== 'function') return reply(200, { ok: false, error: 'no_skill_registry' })
|
|
532
|
+
const agent = ctx.get('agents')?.get?.(session.id)
|
|
533
|
+
let snapshot = null
|
|
534
|
+
try {
|
|
535
|
+
snapshot = await skills.snapshot({ cwd, ...(agent === undefined ? {} : { scope: agent }) })
|
|
536
|
+
} catch {
|
|
537
|
+
return reply(200, { ok: false, error: 'snapshot_failed' })
|
|
538
|
+
}
|
|
539
|
+
// 观测不完整就不发:一张缺几条的表比空表更坏(它会看起来像「技能被删了」)。
|
|
540
|
+
if (snapshot === null || snapshot.complete !== true) return reply(200, { ok: false, error: 'incomplete' })
|
|
541
|
+
const prefix = cwd.endsWith(sep) ? cwd : `${cwd}${sep}`
|
|
542
|
+
const entries = (Array.isArray(snapshot.skills) ? snapshot.skills : [])
|
|
543
|
+
.map((skill) => {
|
|
544
|
+
const base = skill?.resourceBase
|
|
545
|
+
const dir = base !== null && typeof base === 'object' && base.kind === 'directory' && typeof base.path === 'string' ? base.path : null
|
|
546
|
+
return {
|
|
547
|
+
name: String(skill?.name ?? ''),
|
|
548
|
+
description: String(skill?.description ?? '').slice(0, 300),
|
|
549
|
+
when_to_use: typeof skill?.whenToUse === 'string' ? skill.whenToUse.slice(0, 300) : null,
|
|
550
|
+
source: String(skill?.source ?? 'unknown'),
|
|
551
|
+
provider: String(skill?.provider ?? ''),
|
|
552
|
+
model: skill?.invocation?.modelInvocable === true,
|
|
553
|
+
user: skill?.invocation?.userInvocable === true,
|
|
554
|
+
dir,
|
|
555
|
+
// 与内核同一套约定:虚拟条目(记忆索引)没有正文文件。
|
|
556
|
+
file: dir === null || String(skill?.source ?? '') === 'clearai-memory' ? null : join(dir, 'SKILL.md'),
|
|
557
|
+
inside: dir === null ? null : dir === cwd || dir.startsWith(prefix),
|
|
558
|
+
}
|
|
559
|
+
})
|
|
560
|
+
.filter((entry) => entry.name !== '')
|
|
561
|
+
return reply(200, { ok: true, complete: true, entries })
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
ctx.effect(
|
|
567
|
+
() =>
|
|
568
|
+
ctx.provide('clearai', {
|
|
569
|
+
/** 原始状态(工具做不变量判断用)。调用方不得修改。 */
|
|
570
|
+
state: stateOf,
|
|
571
|
+
/** 派生:阶段/完成度/假设状态/世界线阶段,全部现算。 */
|
|
572
|
+
derive: (sessionId) => derive(stateOf(sessionId)),
|
|
573
|
+
/** 面板视图(与 wire 同一份)。 */
|
|
574
|
+
view: (sessionId) => view(stateOf(sessionId)),
|
|
575
|
+
/**
|
|
576
|
+
* 运行态卡:注给模型的**事实**。
|
|
577
|
+
*
|
|
578
|
+
* `overrides.autonomy`:人刚在面板上切过档的那一拍,投影里还是旧档(消息先入 inbox、
|
|
579
|
+
* 后落日志,而 pre-step 跑在它落账之前,§17.5)。卡片必须与这一回合**真正要跑的机制**
|
|
580
|
+
* 同源,否则模型会读到两条互相矛盾的话——实测里它为此停下来问过人。
|
|
581
|
+
*/
|
|
582
|
+
renderCard: (sessionId, overrides = {}) => {
|
|
583
|
+
const state = stateOf(sessionId)
|
|
584
|
+
if (overrides.autonomy !== 'attended' && overrides.autonomy !== 'unattended') return renderCard(state)
|
|
585
|
+
return renderCard({
|
|
586
|
+
...state,
|
|
587
|
+
autonomy: {
|
|
588
|
+
...(state.autonomy ?? {}),
|
|
589
|
+
effective: { value: overrides.autonomy, source: 'session', preset: state.autonomy?.effective?.preset ?? null },
|
|
590
|
+
},
|
|
591
|
+
})
|
|
592
|
+
},
|
|
593
|
+
/**
|
|
594
|
+
* 预演:把本次调用要落的变更先折一遍,好让工具返回的卡片是**这一步之后**的样子。
|
|
595
|
+
* 投影要到结果落账才前进,而工具在返回时就需要说清新状态——
|
|
596
|
+
* 与其在预设侧复制一份 fold,不如让 fold 的拥有者替它算。
|
|
597
|
+
*/
|
|
598
|
+
preview: (sessionId, mutations) => {
|
|
599
|
+
const next = applyMutations(stateOf(sessionId), mutations)
|
|
600
|
+
return { state: next, card: renderCard(next), view: view(next) }
|
|
601
|
+
},
|
|
602
|
+
}),
|
|
603
|
+
'clearai: read facade',
|
|
604
|
+
)
|
|
605
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clearai-dsh",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ClearAI: The Epistemic Loop, native to DSH.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"main": "./lib/host.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./lib/host.js",
|
|
11
|
+
"./client": "./lib/client.js",
|
|
12
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"clearai-dsh": "./bin/clearai.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib",
|
|
20
|
+
"presets",
|
|
21
|
+
"bin",
|
|
22
|
+
"cordis.patch.yml",
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"README.md",
|
|
25
|
+
"README.zh-CN.md",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"brand"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"zod": "^4.6.1"
|
|
34
|
+
},
|
|
35
|
+
"dsh": {
|
|
36
|
+
"bundle": {
|
|
37
|
+
"patch": "./cordis.patch.yml"
|
|
38
|
+
},
|
|
39
|
+
"client": {
|
|
40
|
+
"platform": "web",
|
|
41
|
+
"inject": [
|
|
42
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
44
|
+
"@deepseek-ai/dsh-client-locale"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"dsh-plugin",
|
|
50
|
+
"deepseek-harness",
|
|
51
|
+
"clearai",
|
|
52
|
+
"agent-preset",
|
|
53
|
+
"evidence",
|
|
54
|
+
"worldline"
|
|
55
|
+
],
|
|
56
|
+
"scripts": {
|
|
57
|
+
"test": "bash test/run.sh",
|
|
58
|
+
"build": "node tools/build-package.mjs",
|
|
59
|
+
"verify": "node tools/verify-package.mjs",
|
|
60
|
+
"verify:deploy": "node tools/verify-deploy.mjs",
|
|
61
|
+
"verify:install": "node tools/verify-clean-install.mjs",
|
|
62
|
+
"pack": "node tools/build-package.mjs && npm pack ./dist/clearai-dsh",
|
|
63
|
+
"release": "node tools/build-package.mjs && node tools/verify-package.mjs && node tools/verify-clean-install.mjs && npm pack ./dist/clearai-dsh --pack-destination dist"
|
|
64
|
+
}
|
|
65
|
+
}
|