lantern-harness 0.1.2 → 0.1.3
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 +61 -0
- package/README.md +6 -6
- package/builtin/session/lh-spaces.mjs +102 -0
- package/cli/data-root.mjs +80 -3
- package/cli/lantern.mjs +2 -1
- package/cli/server.mjs +4 -0
- package/cli/spaces.mjs +91 -5
- package/config/app.defaults.json +4 -2
- package/dist/500.html +9 -5
- package/dist/_astro/Base.ByB0D4ZA.css +1 -0
- package/dist/_astro/{Base.astro_astro_type_script_index_0_lang.lxEwQ1R4.js → Base.astro_astro_type_script_index_0_lang.Cg_8YLVA.js} +1 -1
- package/dist/_astro/CaseGenerateApp.BlZyjWEr.js +9 -0
- package/dist/_astro/CasesLibrary.C76La3Xj.js +1 -0
- package/dist/_astro/ChatApp.gfsPLvan.js +269 -0
- package/dist/_astro/ColumnResize.Bs6GL3DJ.js +1 -0
- package/dist/_astro/DatePicker.BdwFRioL.js +1 -0
- package/dist/_astro/{DocsFileBrowser.DkE26UTR.js → DocsFileBrowser.BxAyrkkw.js} +1 -1
- package/dist/_astro/KnowledgeLibrary.Cv488Hot.js +1 -0
- package/dist/_astro/NavLibraries.ALZ9e12z.js +1 -0
- package/dist/_astro/PluginsPage.B2uxCiHL.js +1 -0
- package/dist/_astro/{PluginsPage.BNzpPg2H.js → PluginsPage.DEkyOfQE.js} +2 -2
- package/dist/_astro/PromptsLibrary.BUC1OiCH.js +1 -0
- package/dist/_astro/RequirementsLibrary.D3olQI63.js +1 -0
- package/dist/_astro/{SearchableSelect.DMS2XmXc.js → SearchableSelect.DP74JRWW.js} +1 -1
- package/dist/_astro/SelectDropdown.D2i7-lQC.js +1 -0
- package/dist/_astro/{SettingsDialog.DC0e7580.js → SettingsDialog.CrMDjSLJ.js} +1 -1
- package/dist/_astro/SlideOverDrawer.C7UVeCX1.js +1 -0
- package/dist/_astro/SpaceSwitcher.Wf8EPYUx.js +7 -0
- package/dist/_astro/TasksBoard.2j7rGT9O.js +1 -0
- package/dist/_astro/TemplatesLibrary.J0x7yIcY.js +1 -0
- package/dist/_astro/TodosBoard.BugeB7_7.js +1 -0
- package/dist/_astro/app-config.vqDkEMS3.js +1 -0
- package/dist/_astro/doc-roots.DAJP9Svr.js +1 -0
- package/dist/_astro/i18n.9qZsrs3u.js +7 -0
- package/dist/_astro/{open-docs-in-session.D3JCwMWB.js → open-docs-in-session.fLwnA5qh.js} +1 -1
- package/dist/_astro/plugins.DSUPSizB.js +1 -0
- package/dist/_astro/project-docs.Cd2gKPKh.js +1 -0
- package/dist/_astro/{settings.BoQ0e0m1.js → settings.CkbipdPI.js} +2 -2
- package/dist/_astro/{useListAnim.CnbC_9MC.js → useListAnim.Y3N2budK.js} +1 -1
- package/dist/cases/generate/index.html +10 -6
- package/dist/cases/library/index.html +10 -6
- package/dist/docs/changelog/index.html +69 -6
- package/dist/docs/install/index.html +22 -8
- package/dist/docs/manual/index.html +74 -11
- package/dist/index.html +10 -6
- package/dist/knowledge/index.html +10 -6
- package/dist/knowledge//345/212/237/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/347/274/226/345/206/231/350/247/204/350/214/203/index.html +9 -5
- package/dist/plugins/index.html +10 -6
- package/dist/prompts/index.html +10 -6
- package/dist/requirements/index.html +10 -6
- package/dist/tasks/index.html +253 -0
- package/dist/templates/index.html +10 -6
- package/dist/templates//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277/index.html +9 -5
- package/dist/todos/index.html +253 -0
- package/docs//345/205/250/345/261/200/346/217/222/344/273/266.md +2 -2
- package/docs//345/256/211/350/243/205/346/211/213/345/206/214.md +10 -8
- package/docs//346/223/215/344/275/234/346/211/213/345/206/214.md +73 -23
- package/lh-plugins/lh-project-docs.mjs +2 -0
- package/lh-plugins/lh-tasks.mjs +223 -0
- package/lh-plugins/lh-todos.mjs +297 -0
- package/lh-plugins/storage-domain.mjs +1 -0
- package/package.json +1 -1
- package/dist/_astro/Base.B9f_iFAZ.css +0 -1
- package/dist/_astro/CaseGenerateApp.Br4uKjun.js +0 -9
- package/dist/_astro/CasesLibrary.CpPoVQ4P.js +0 -1
- package/dist/_astro/ChatApp.CYSPlSNP.js +0 -258
- package/dist/_astro/KnowledgeLibrary.DZinlAWn.js +0 -1
- package/dist/_astro/NavLibraries.EayBsg5g.js +0 -1
- package/dist/_astro/PluginsPage.CY61glLU.js +0 -1
- package/dist/_astro/PromptsLibrary.DfrrCjs7.js +0 -1
- package/dist/_astro/RequirementsLibrary.Dt0Gj6Nr.js +0 -1
- package/dist/_astro/SelectDropdown.Dc7QaPbH.js +0 -1
- package/dist/_astro/SpaceSwitcher.Yy2w8MDF.js +0 -7
- package/dist/_astro/TemplatesLibrary.uK-SjK1z.js +0 -1
- package/dist/_astro/app-config.CpjKWnzM.js +0 -1
- package/dist/_astro/i18n.BVvhz-vw.js +0 -7
- package/dist/_astro/plugins.CrpY8UI9.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# 版本日志
|
|
2
2
|
|
|
3
|
+
## [0.1.3] - 2026-10-18
|
|
4
|
+
|
|
5
|
+
新增「待办与效率」两个插件(任务管理 / 待办事项)并把提示词管理改为内置常开项;空间管理弹窗重做为单层卡片式流程,空间复制不再串数据。
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
#### 待办与效率
|
|
10
|
+
|
|
11
|
+
- 新插件分类「待办与效率」,内建两个独立插件,数据按空间隔离:任务存 `<空间>/.lantern/local/tasks.json`,待办存 `<空间>/.lantern/local/todos.json`,切换空间看到各自的数据
|
|
12
|
+
- 任务管理:任务池 / 进行中 / 已完成 / 已关闭四态,顶部六张统计卡片(全部 / 任务池 / 进行中 / 已完成 / 已关闭 / 需要紧急);行内改状态与紧急程度、点日期弹卡片日历、任务周期自动计算;「待办数量」「完成进度」按待办里的「所属任务」实时汇总;右侧抽屉可改名称 / 日期 / 备注并查看已关联待办
|
|
13
|
+
- 待办事项:飞书多维表格样式的列表,按状态分组(进行中 / 待开始 / 已延期 / 已完成 / 已取消)、组标题可折叠,分组方式还可切为「按紧急程度」或「不分组」
|
|
14
|
+
- 待办行内直接改名称 / 状态 / 紧急程度 / 所属任务 / 开始与截止日期 / 备注;状态下拉与紧急程度用颜色胶囊区分,不显示对钩;备注与名称超长省略号显示
|
|
15
|
+
- 每个分组底部「+ 新增记录」:展开一行填完即保存(回车或点空白处),没填任何内容时点空白处自动收起,Esc 放弃;顶部「新建待办」从右侧滑出详情抽屉
|
|
16
|
+
- **结束日期改为只读派生值**:状态改为「已完成 / 已取消」时自动记为当天,之后再改其它字段不会刷新;状态改回「待开始 / 进行中」时清空并显示「待办未完成」;表格、抽屉、AI 工具改状态都按同一规则处理
|
|
17
|
+
- 待办的「所属任务」下拉不出已关闭的任务;待办关联任务时把任务从「任务池 / 已完成」拉回「进行中」(已关闭的不动),打开待办页时也会对既有未完成待办做一次同样对齐
|
|
18
|
+
- 新建待办 / 任务的开始日期、截止日期默认当天
|
|
19
|
+
- 列宽可拖:鼠标移到表头字段右边缘左右拖动即可,宽度按页面记在浏览器本地;宽屏下保持紧凑间距,深色主题下表格、胶囊、日历均已适配
|
|
20
|
+
- 工作台「+」→「计划」下面新增「待办」「任务」手动入口(对应插件关闭时该入口隐藏):复用新增前的确认卡片,待办分两页(第一页字段、第二页选所属任务并只读展示任务的名称 / 状态 / 紧急程度 / 起止日期,选中后自动带日期与紧急程度到第一页);手动入口的最后一个按钮是「提交」,AI 触发的仍是「确认新增」;点「取消」或卡片外面只播收起动画,不写入数据
|
|
21
|
+
- 工作台「回合」右侧新增「待办」Tab:默认「全部待办」,按 已逾期 / 今天截止 / 接下来 / 没有截止日期 / 已完成 分桶,也可只看未完成或已完成;每行显示名称、紧急程度、截止日期、剩余天数与所属任务,点左侧圆圈标记完成,右上角跳转完整待办页,停留时每 15 秒自动刷新
|
|
22
|
+
- AI 工具:`todo_add` / `todo_list` / `todo_update` / `todo_delete`、`task_add` / `task_list` / `task_update` / `task_delete`,写入前统一弹确认卡片
|
|
23
|
+
|
|
24
|
+
#### 空间管理
|
|
25
|
+
|
|
26
|
+
- 空间管理弹窗重做为单层卡片:默认展示全部空间,「+ 新增空间」直接在弹窗内展开,点击取消回到全部空间;新建空间不再二次弹窗,去掉了无用的「完成」按钮
|
|
27
|
+
- 新增「编辑空间」:可改名称、提示词要求与默认提示词;公共空间名称为锁定状态;保存后当前空间的默认提示词立即生效
|
|
28
|
+
- 新建与编辑共用「提示词要求」输入框:回车触发生成,Shift+回车换行,随内容自动增高
|
|
29
|
+
- 每个空间都补齐「提示词管理 / 默认提示词」,缺失时按包内种子补一份;空间里另存一份「提示词要求」(`.lantern/config/space-brief.json`),便于之后重新生成
|
|
30
|
+
- 空间复制补齐收尾:复制品的工作区注册表改指新目录(此前新空间打开时「工作区」仍停在源空间);跳过会话检索 sqlite(含 WAL / shm)与 spill 等可重建缓存;仍有会话在运行时拒绝复制并提示先停止
|
|
31
|
+
- 重命名当前空间后服务端数据根同步指向新目录,不再需要重启或手动切换空间
|
|
32
|
+
- 外部纳入的空间在已有 `.lantern` 元数据时「删除数据」按真实删除处理,不再只移出列表留下空壳
|
|
33
|
+
|
|
34
|
+
#### 插件与导航
|
|
35
|
+
|
|
36
|
+
- 提示词管理改为**内置常开项**:每个空间都需要默认提示词,插件页显示「内置」且不提供开关;入口固定到侧栏「帮助中心」上方,不参与分组与手动排序
|
|
37
|
+
- 侧栏新增「任务管理」「待办事项」入口,随插件开关显示 / 隐藏;「全局插件」文档补上 `todos` / `tasks`
|
|
38
|
+
- 菜单三点弹窗里的「选择分组」改为「加入分组」
|
|
39
|
+
- 提示词管理页移除「在会话中打开」按钮
|
|
40
|
+
- 新增通用组件:列宽拖拽、卡片式日期选择(去掉原生日历图标,底部「清除 / 今天」)、新增前确认卡片、工作台待办 Tab;`SelectDropdown` 支持自定义触发器与选项前缀、无边框样式、菜单最小宽度
|
|
41
|
+
|
|
42
|
+
### 变更
|
|
43
|
+
|
|
44
|
+
- 新建空间默认开启「提示词管理」,侧栏默认是「我的工作台」「提示词管理」「帮助中心」;其余插件仍默认关闭
|
|
45
|
+
- 空间固有内容不再受插件开关影响:提示词管理目录与默认提示词始终存在,旧版本铺出的空壳目录照旧收起
|
|
46
|
+
- 待办抽屉里的「任务结束日期」改称「结束日期」,由日期选择器改为只读文本
|
|
47
|
+
- 版本日志页:没有导语的版本会按小节自动生成一句概览,不再只剩标题
|
|
48
|
+
|
|
49
|
+
### 修复
|
|
50
|
+
|
|
51
|
+
- 菜单排序列表里不再出现已关闭插件的条目
|
|
52
|
+
- 编辑空间时不再出现 notfound;新建空间取消后正确回到全部空间列表;新建按钮不可点时有明确的禁用鼠标状态
|
|
53
|
+
- 无数据时待办 / 任务页只保留右上角新建按钮,仍可正常新建
|
|
54
|
+
- 创建确认卡片点卡片外面关闭时带退场动画且不落库,同时修掉该组件的 Fast Refresh 报错
|
|
55
|
+
- 工作台「待办」Tab 在插件关闭时自动退回「对话」,不会停在空白页
|
|
56
|
+
|
|
57
|
+
### 文档
|
|
58
|
+
|
|
59
|
+
- 操作手册补「任务管理」「待办事项」两节(字段、分组、内联新增、结束日期规则、任务关联),以及工作台「+」手动新增、确认卡片两页流程与待办 Tab
|
|
60
|
+
- 操作手册补空间管理的新建 / 编辑、复制空间的注意事项(会话运行中不可复制)与删除外部空间的判定
|
|
61
|
+
- 安装手册去掉框架版本信息,首次进入的默认插件表与侧栏说明按「提示词管理内置常开」更新;README 功能表与「全局插件」规格同步补上 `todos` / `tasks`
|
|
62
|
+
|
|
3
63
|
## [0.1.2] - 2026-10-18
|
|
4
64
|
|
|
5
65
|
### 变更
|
|
@@ -163,6 +223,7 @@ Lantern Harness 首个版本:AI 工作台。一份进程里隔离多套项目
|
|
|
163
223
|
- 开发服忽略打开内容库目录与空间元数据目录导致的整页热更新
|
|
164
224
|
- 文档与 CLI 统一 pnpm 优先:安装 / 卸载 / 版本列表优先调用 pnpm(未安装则回退 npm);README、安装手册、操作手册与贡献约定的开发命令改为 pnpm
|
|
165
225
|
|
|
226
|
+
[0.1.3]: https://www.npmjs.com/package/lantern-harness/v/0.1.3
|
|
166
227
|
[0.1.2]: https://www.npmjs.com/package/lantern-harness/v/0.1.2
|
|
167
228
|
[0.1.1]: https://www.npmjs.com/package/lantern-harness/v/0.1.1
|
|
168
229
|
[0.1.0]: https://www.npmjs.com/package/lantern-harness/v/0.1.0
|
package/README.md
CHANGED
|
@@ -8,11 +8,11 @@ AI 工作台:对话与插件工具、需求 / 用例 / 知识 / 提示词内
|
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
11
|
-
| 模块 | 说明
|
|
12
|
-
| --------- |
|
|
13
|
-
| AI 工作台 | 多轮对话,流式输出,BYOK(自带密钥)直连模型;默认提示词按空间可改,可按插件挂载工具
|
|
14
|
-
| 会话记忆 | 五轨记忆(用户档案 / 全局事实 / 项目关键 / 项目日志 / 每日),待确认队列与工作台「记忆」Tab
|
|
15
|
-
| 插件库 | 内置能力开关:内容库(需求 / 用例 / 提示词 / 知识库 /
|
|
11
|
+
| 模块 | 说明 |
|
|
12
|
+
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| AI 工作台 | 多轮对话,流式输出,BYOK(自带密钥)直连模型;默认提示词按空间可改,可按插件挂载工具 |
|
|
14
|
+
| 会话记忆 | 五轨记忆(用户档案 / 全局事实 / 项目关键 / 项目日志 / 每日),待确认队列与工作台「记忆」Tab |
|
|
15
|
+
| 插件库 | 内置能力开关:内容库(需求 / 用例 / 提示词 / 知识库 / 模板)、生成能力、记忆与检索、任务与待办、开发工具;开启后侧栏出现对应入口,也可让模型推荐能力建议 |
|
|
16
16
|
|
|
17
17
|
模型配置(BYOK)、内容目录约定、插件用法见 **[操作手册](https://lantern.yychuiyan.com/docs/manual/)**。
|
|
18
18
|
|
|
@@ -76,7 +76,7 @@ lantern acp # 以 ACP 子集供外部编排驱动无 UI Agent
|
|
|
76
76
|
|
|
77
77
|
4、 插件默认值:
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
新建空间默认开启「提示词管理」(每个空间都要有默认提示词,属于内置常开项),侧栏为「我的工作台」「提示词管理」「帮助中心」。其余扩展插件(其它内容库、生成能力、知识库检索、会话记忆、任务管理 / 待办事项、插件开发)都要在「插件」页按分类手动开启;由「插件开发」新增的插件同样默认关闭。
|
|
80
80
|
|
|
81
81
|
### 方式二:源码启动
|
|
82
82
|
|
|
@@ -15,8 +15,12 @@ import os from 'node:os';
|
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
|
+
ensureSpaceSystemPrompt,
|
|
18
19
|
prepareExternalSpaceDir,
|
|
19
20
|
prepareSpaceDir,
|
|
21
|
+
readSpaceBrief,
|
|
22
|
+
readSpaceSystemPrompt,
|
|
23
|
+
writeSpaceBrief,
|
|
20
24
|
writeSpaceSystemPrompt,
|
|
21
25
|
} from '../../cli/data-root.mjs';
|
|
22
26
|
import {
|
|
@@ -207,6 +211,21 @@ function syncActiveRoot() {
|
|
|
207
211
|
return { ok: true, id: activeId, changed: true };
|
|
208
212
|
}
|
|
209
213
|
|
|
214
|
+
/** 重命名的是当前空间时,把服务的数据根指到新目录(否则后续读写仍指向旧路径) */
|
|
215
|
+
function retargetActiveRoot(space) {
|
|
216
|
+
if (!space || getActiveSpaceId() !== space.id) return;
|
|
217
|
+
try {
|
|
218
|
+
const nextRoot = prepareSpaceDir(space.path);
|
|
219
|
+
if (path.resolve(nextRoot) === path.resolve(getDataRoot())) return;
|
|
220
|
+
setActiveRoot(nextRoot);
|
|
221
|
+
closeAllBashSessions();
|
|
222
|
+
closeAllPwshSessions();
|
|
223
|
+
syncRunState(nextRoot);
|
|
224
|
+
} catch {
|
|
225
|
+
// 名称已改;路径对齐失败时下次切换空间会自愈
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
210
229
|
export function lhSpacesPlugin() {
|
|
211
230
|
return {
|
|
212
231
|
name: 'lh-spaces',
|
|
@@ -257,6 +276,8 @@ export function lhSpacesPlugin() {
|
|
|
257
276
|
try {
|
|
258
277
|
prepareSpaceDir(created.space.path);
|
|
259
278
|
if (promptText.trim()) writeSpaceSystemPrompt(created.space.path, promptText);
|
|
279
|
+
const brief = typeof body.promptBrief === 'string' ? body.promptBrief.trim() : '';
|
|
280
|
+
if (brief) writeSpaceBrief(created.space.path, brief);
|
|
260
281
|
} catch (e) {
|
|
261
282
|
return sendJson(res, 500, {
|
|
262
283
|
ok: false,
|
|
@@ -280,11 +301,92 @@ export function lhSpacesPlugin() {
|
|
|
280
301
|
const body = await readJson(req);
|
|
281
302
|
const out = renameSpace(body.id, body.name, getBaseRoot());
|
|
282
303
|
if (!out.ok) return sendJson(res, 400, { ok: false, error: out.error });
|
|
304
|
+
retargetActiveRoot(out.space);
|
|
283
305
|
return sendJson(res, 200, { ok: true, space: out.space });
|
|
284
306
|
}
|
|
285
307
|
|
|
308
|
+
// 空间详情:名称 / 提示词要求 / 默认提示词,供管理面板编辑
|
|
309
|
+
if (url === `${PREFIX}/detail` && req.method === 'GET') {
|
|
310
|
+
const q = new URLSearchParams(req.url.split('?')[1] ?? '');
|
|
311
|
+
const resolved = resolveSpace(q.get('id') ?? '', getBaseRoot());
|
|
312
|
+
if (!resolved.ok) return sendJson(res, 404, { ok: false, error: resolved.error });
|
|
313
|
+
const space = resolved.space;
|
|
314
|
+
// 外部纳入的空间不铺骨架;托管空间补齐默认提示词文件
|
|
315
|
+
if (!space.external) {
|
|
316
|
+
try {
|
|
317
|
+
prepareSpaceDir(space.path);
|
|
318
|
+
} catch {
|
|
319
|
+
// 目录暂时不可写时仍然返回已有内容
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return sendJson(res, 200, {
|
|
323
|
+
ok: true,
|
|
324
|
+
space,
|
|
325
|
+
brief: readSpaceBrief(space.path),
|
|
326
|
+
systemPrompt: readSpaceSystemPrompt(space.path),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 保存空间名称 / 提示词要求 / 默认提示词
|
|
331
|
+
if (url === `${PREFIX}/update` && req.method === 'POST') {
|
|
332
|
+
const body = await readJson(req);
|
|
333
|
+
const resolved = resolveSpace(body.id, getBaseRoot());
|
|
334
|
+
if (!resolved.ok) return sendJson(res, 404, { ok: false, error: resolved.error });
|
|
335
|
+
let space = resolved.space;
|
|
336
|
+
|
|
337
|
+
const nextName = typeof body.name === 'string' ? body.name.trim() : '';
|
|
338
|
+
if (nextName && nextName !== space.name) {
|
|
339
|
+
if (space.isPublic) {
|
|
340
|
+
return sendJson(res, 400, {
|
|
341
|
+
ok: false,
|
|
342
|
+
error: `${PUBLIC_SPACE_NAME}不支持重命名`,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const renamed = renameSpace(space.id, nextName, getBaseRoot());
|
|
346
|
+
if (!renamed.ok) return sendJson(res, 400, { ok: false, error: renamed.error });
|
|
347
|
+
space = renamed.space;
|
|
348
|
+
retargetActiveRoot(space);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (typeof body.brief === 'string') {
|
|
352
|
+
if (body.brief.length > 20_000) {
|
|
353
|
+
return sendJson(res, 400, { ok: false, error: '提示词要求过长' });
|
|
354
|
+
}
|
|
355
|
+
writeSpaceBrief(space.path, body.brief);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (typeof body.systemPrompt === 'string' && body.systemPrompt.trim()) {
|
|
359
|
+
if (body.systemPrompt.length > 80_000) {
|
|
360
|
+
return sendJson(res, 400, { ok: false, error: '默认提示词过长' });
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
if (!space.external) ensureSpaceSystemPrompt(space.path);
|
|
364
|
+
writeSpaceSystemPrompt(space.path, body.systemPrompt);
|
|
365
|
+
} catch (e) {
|
|
366
|
+
return sendJson(res, 500, {
|
|
367
|
+
ok: false,
|
|
368
|
+
error: e instanceof Error ? e.message : String(e),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return sendJson(res, 200, {
|
|
374
|
+
ok: true,
|
|
375
|
+
space,
|
|
376
|
+
brief: readSpaceBrief(space.path),
|
|
377
|
+
systemPrompt: readSpaceSystemPrompt(space.path),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
286
381
|
if (url === `${PREFIX}/copy` && req.method === 'POST') {
|
|
287
382
|
const body = await readJson(req);
|
|
383
|
+
// 复制是逐文件拷贝:会话在跑时源空间仍在写,拷出来可能半份
|
|
384
|
+
if (hasRunningSessions()) {
|
|
385
|
+
return sendJson(res, 409, {
|
|
386
|
+
ok: false,
|
|
387
|
+
error: '仍有会话在运行,请先停止后再复制空间',
|
|
388
|
+
});
|
|
389
|
+
}
|
|
288
390
|
const out = copySpace(body.id, body.name, getBaseRoot());
|
|
289
391
|
if (!out.ok) return sendJson(res, 400, { ok: false, error: out.error });
|
|
290
392
|
return sendJson(res, 200, { ok: true, space: out.space });
|
package/cli/data-root.mjs
CHANGED
|
@@ -202,13 +202,16 @@ export function prepareSpaceDir(spaceDir) {
|
|
|
202
202
|
fs.copyFileSync(PKG_DEFAULTS, destDefaults);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
//
|
|
206
|
-
//
|
|
205
|
+
// 内容目录按插件开关懒建:插件全关的新空间就是一个干净目录(提示词管理例外,
|
|
206
|
+
// 它承载每个空间必备的「默认提示词」),不会先铺出「用例管理 / 知识库管理」这类入口。
|
|
207
207
|
for (const { dirName, legacyDirNames } of Object.values(DOC_ROOT_DIRS)) {
|
|
208
208
|
// 必须先迁移:一旦先建出中文目录,旧目录里的文件就再也不会被搬过来
|
|
209
209
|
migrateLegacyRoot(root, legacyDirNames, dirName);
|
|
210
210
|
const dir = resolveContentRoot(root, dirName).abs;
|
|
211
|
-
|
|
211
|
+
// 提示词管理是空间的固有部分:每个空间都有「默认提示词」,不受插件开关影响。
|
|
212
|
+
// 新建空间 / npm 安装后首次启动都要把它铺出来,否则提示词页无内容可显示。
|
|
213
|
+
const alwaysOn = dirName === DOC_ROOT_DIRS.prompts.dirName;
|
|
214
|
+
const enabled = alwaysOn || isContentRootEnabled(root, dirName);
|
|
212
215
|
// 旧版本会给所有内容目录铺一个只有 .gitkeep 的空壳:插件没启用就收掉,
|
|
213
216
|
// 让升级后的数据空间和全新安装保持一致(有真实内容的目录绝不动)
|
|
214
217
|
if (!enabled && isDir(dir) && onlyPlaceholders(dir)) {
|
|
@@ -224,6 +227,12 @@ export function prepareSpaceDir(spaceDir) {
|
|
|
224
227
|
|
|
225
228
|
// 种子只补进已存在的目录(种子晚于建目录,这样「目录已存在」不会阻止种子落位)
|
|
226
229
|
seedSpaceDir(root);
|
|
230
|
+
// 提示词是空间固有内容:种子缺失或旧空间没铺过时也补一份默认提示词
|
|
231
|
+
try {
|
|
232
|
+
ensureSpaceSystemPrompt(root);
|
|
233
|
+
} catch {
|
|
234
|
+
// 只读 / 权限异常时不阻断空间准备,提示词页会回退到空内容
|
|
235
|
+
}
|
|
227
236
|
|
|
228
237
|
// 新建空间:种子不算「用户搬进来的内容」,别让它触发插件自动开启
|
|
229
238
|
if (freshSpace) markNavRootsDetected(root);
|
|
@@ -374,6 +383,74 @@ export function seedSpaceDir(spaceDir) {
|
|
|
374
383
|
return planted;
|
|
375
384
|
}
|
|
376
385
|
|
|
386
|
+
/**
|
|
387
|
+
* 确保空间有「提示词管理」目录与「默认提示词」文件(缺失时用包内种子补上)。
|
|
388
|
+
* 幂等:提示词页、空间管理编辑面板都靠它拿到当前空间的默认提示词。
|
|
389
|
+
* @param {string} spaceDir
|
|
390
|
+
* @returns {string} 默认提示词文件的绝对路径
|
|
391
|
+
*/
|
|
392
|
+
export function ensureSpaceSystemPrompt(spaceDir) {
|
|
393
|
+
const root = path.resolve(spaceDir);
|
|
394
|
+
const dir = resolveContentRoot(root, DOC_ROOT_DIRS.prompts.dirName).abs;
|
|
395
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
396
|
+
const dest = path.join(dir, SPACE_SYSTEM_PROMPT_FILE);
|
|
397
|
+
if (!fs.existsSync(dest)) {
|
|
398
|
+
const seed = path.join(PKG_SEEDS, DOC_ROOT_DIRS.prompts.dirName, SPACE_SYSTEM_PROMPT_FILE);
|
|
399
|
+
try {
|
|
400
|
+
if (fs.existsSync(seed)) {
|
|
401
|
+
const buf = fs.readFileSync(seed);
|
|
402
|
+
fs.writeFileSync(dest, buf);
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
// 种子只是初始内容:失败不阻断读取,前台会显示空提示词
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return dest;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** 读取空间默认提示词正文(缺失时返回空串,不改动磁盘) */
|
|
412
|
+
export function readSpaceSystemPrompt(spaceDir) {
|
|
413
|
+
try {
|
|
414
|
+
const fp = path.join(
|
|
415
|
+
resolveContentRoot(path.resolve(spaceDir), DOC_ROOT_DIRS.prompts.dirName).abs,
|
|
416
|
+
SPACE_SYSTEM_PROMPT_FILE,
|
|
417
|
+
);
|
|
418
|
+
if (!fs.existsSync(fp)) return '';
|
|
419
|
+
return fs.readFileSync(fp, 'utf8').replace(/\r\n/g, '\n').trim();
|
|
420
|
+
} catch {
|
|
421
|
+
return '';
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** 空间的「提示词要求」:跟着空间走的小段描述,存 `.lantern/config/space-brief.json` */
|
|
426
|
+
export function readSpaceBrief(spaceDir) {
|
|
427
|
+
try {
|
|
428
|
+
const raw = JSON.parse(
|
|
429
|
+
fs.readFileSync(spaceConfigFile(path.resolve(spaceDir), 'space-brief.json'), 'utf8'),
|
|
430
|
+
);
|
|
431
|
+
return typeof raw?.brief === 'string' ? raw.brief : '';
|
|
432
|
+
} catch {
|
|
433
|
+
return '';
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 写入空间「提示词要求」(允许写空串,用于清空)。
|
|
439
|
+
* @param {string} spaceDir
|
|
440
|
+
* @param {string} brief
|
|
441
|
+
* @returns {boolean}
|
|
442
|
+
*/
|
|
443
|
+
export function writeSpaceBrief(spaceDir, brief) {
|
|
444
|
+
try {
|
|
445
|
+
const dest = spaceConfigFile(path.resolve(spaceDir), 'space-brief.json');
|
|
446
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
447
|
+
fs.writeFileSync(dest, `${JSON.stringify({ brief: String(brief ?? '') }, null, 2)}\n`, 'utf8');
|
|
448
|
+
return true;
|
|
449
|
+
} catch {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
377
454
|
/**
|
|
378
455
|
* 把生成/手写的提示词写成该空间的默认提示词(覆盖种子,不覆盖空串)。
|
|
379
456
|
* @param {string} spaceDir
|
package/cli/lantern.mjs
CHANGED
|
@@ -320,8 +320,9 @@ function cmdStart({ foreground = false, dir = null } = {}) {
|
|
|
320
320
|
console.log('Lantern Harness 已在后台启动');
|
|
321
321
|
console.log(` 地址:${state.url}`);
|
|
322
322
|
console.log(` 当前空间:${state.dataRoot}`);
|
|
323
|
-
console.log(` 日志:${logPath}`);
|
|
324
323
|
console.log(' 停止:lantern stop');
|
|
324
|
+
console.log(' 更新:lantern update');
|
|
325
|
+
console.log(' 更多命令:lantern help');
|
|
325
326
|
console.log('');
|
|
326
327
|
return;
|
|
327
328
|
}
|
package/cli/server.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import { lhSpillPlugin } from '../builtin/session/lh-spill.mjs';
|
|
|
16
16
|
import { lhKbPlugin } from '../lh-plugins/lh-kb.mjs';
|
|
17
17
|
import { lhMemoryPlugin } from '../lh-plugins/lh-memory.mjs';
|
|
18
18
|
import { lhResourcesPlugin } from '../lh-plugins/lh-resources.mjs';
|
|
19
|
+
import { lhTodosPlugin } from '../lh-plugins/lh-todos.mjs';
|
|
20
|
+
import { lhTasksPlugin } from '../lh-plugins/lh-tasks.mjs';
|
|
19
21
|
import { lhWebPlugin } from '../builtin/session/lh-web.mjs';
|
|
20
22
|
import { tbSessionRunsPlugin } from '../builtin/session/session-runs.mjs';
|
|
21
23
|
import { lhSpacesPlugin } from '../builtin/session/lh-spaces.mjs';
|
|
@@ -48,6 +50,8 @@ function attachPlugins(app, dataRoot) {
|
|
|
48
50
|
const plugins = [
|
|
49
51
|
tbSessionRunsPlugin(),
|
|
50
52
|
lhSpacesPlugin(),
|
|
53
|
+
lhTodosPlugin(),
|
|
54
|
+
lhTasksPlugin(),
|
|
51
55
|
lhNavGroupsPlugin(),
|
|
52
56
|
lhConfigPlugin(),
|
|
53
57
|
lhResourcesPlugin(),
|
package/cli/spaces.mjs
CHANGED
|
@@ -791,6 +791,10 @@ export function copySpace(id, nextName, baseRoot) {
|
|
|
791
791
|
return { ok: false, error: `复制失败:${e instanceof Error ? e.message : String(e)}` };
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
+
// 复制品里的工作区注册表还指着源空间的绝对路径,必须改指新目录,
|
|
795
|
+
// 否则新空间打开时「工作区」停在被复制的那个空间上。
|
|
796
|
+
retargetCopiedWorkspaces(dir, resolved.space.path);
|
|
797
|
+
|
|
794
798
|
const entry = {
|
|
795
799
|
id: newSpaceId(),
|
|
796
800
|
name: clean.name,
|
|
@@ -809,6 +813,69 @@ export function copySpace(id, nextName, baseRoot) {
|
|
|
809
813
|
return { ok: true, space: entry };
|
|
810
814
|
}
|
|
811
815
|
|
|
816
|
+
/**
|
|
817
|
+
* 把复制品里的工作区注册表改指新目录。
|
|
818
|
+
*
|
|
819
|
+
* 源空间的 `workspaces.json` 记的是源目录的绝对路径(`active` 与 `ws_project` 条目);
|
|
820
|
+
* 直接拷过来后,源目录仍真实存在,`ensureWorkspaceRegistry` 的「父目录 / 不存在」清理都不会命中,
|
|
821
|
+
* 于是新空间的工作区停在源空间上。这里把指向源目录的条目改名到新目录,其余条目保持不动。
|
|
822
|
+
*/
|
|
823
|
+
function retargetCopiedWorkspaces(newDir, sourceDir) {
|
|
824
|
+
const fp = path.join(newDir, LOCAL_REL, 'workspaces.json');
|
|
825
|
+
const target = path.resolve(newDir);
|
|
826
|
+
const source = path.resolve(sourceDir);
|
|
827
|
+
const folderName = (abs) => path.basename(abs) || abs;
|
|
828
|
+
let raw;
|
|
829
|
+
try {
|
|
830
|
+
raw = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
831
|
+
} catch {
|
|
832
|
+
// 源空间没有工作区注册表(或已损坏):不补,首次打开会按空间根自建
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (!raw || typeof raw !== 'object') return;
|
|
836
|
+
|
|
837
|
+
const items = (Array.isArray(raw.items) ? raw.items : []).map((it) => {
|
|
838
|
+
const p = String(it?.path ?? '');
|
|
839
|
+
if (!p || path.resolve(p) !== source) return it;
|
|
840
|
+
return {
|
|
841
|
+
...it,
|
|
842
|
+
id: 'ws_project',
|
|
843
|
+
path: target,
|
|
844
|
+
name: folderName(target),
|
|
845
|
+
addedAt: Number(it?.addedAt) || 0,
|
|
846
|
+
};
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// 复制品必须有自己的根工作区(源注册表里没有源目录条目时补一条)
|
|
850
|
+
if (!items.some((it) => path.resolve(String(it?.path ?? '')) === target)) {
|
|
851
|
+
items.unshift({ id: 'ws_project', path: target, name: folderName(target), addedAt: 0 });
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const activeRaw = String(raw.active ?? '');
|
|
855
|
+
const active =
|
|
856
|
+
!activeRaw || path.resolve(activeRaw) === source ? target : path.resolve(activeRaw);
|
|
857
|
+
|
|
858
|
+
try {
|
|
859
|
+
fs.writeFileSync(
|
|
860
|
+
fp,
|
|
861
|
+
`${JSON.stringify(
|
|
862
|
+
{
|
|
863
|
+
active,
|
|
864
|
+
// 插件开发借用包根是临时态,不跟着复制
|
|
865
|
+
activeHarness: false,
|
|
866
|
+
items,
|
|
867
|
+
updatedAt: Date.now(),
|
|
868
|
+
},
|
|
869
|
+
null,
|
|
870
|
+
2,
|
|
871
|
+
)}\n`,
|
|
872
|
+
'utf8',
|
|
873
|
+
);
|
|
874
|
+
} catch {
|
|
875
|
+
// 写不进去不影响空间可用性,下次打开会按空间根自愈
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
812
879
|
/**
|
|
813
880
|
* 从列表移除空间。
|
|
814
881
|
* @param {{ id: string, purge?: boolean, baseRoot: string }} opts purge=true 时连磁盘数据一起删
|
|
@@ -830,8 +897,11 @@ export function removeSpace({ id, purge = false, baseRoot }) {
|
|
|
830
897
|
if (!purge) return { ok: true, purged: false, space };
|
|
831
898
|
|
|
832
899
|
// 用户手工纳入的外部文件夹:没有空间元数据就不是我们铺的目录,
|
|
833
|
-
//
|
|
834
|
-
|
|
900
|
+
// 只移出列表,绝不代删(那可能是他自己的资料文件夹)。
|
|
901
|
+
// 例外:目录里已经有 `.lantern` 元数据(会话 / 配置 / 记忆),说明确实被本应用
|
|
902
|
+
// 当成数据空间用过——用户点「删除数据」后还留着空壳只会让人以为没删掉。
|
|
903
|
+
const hasLanternMeta = fs.existsSync(spaceMetaDir(space.path));
|
|
904
|
+
if (space.external && !resolveManifestPath(space.path) && !hasLanternMeta) {
|
|
835
905
|
return {
|
|
836
906
|
ok: true,
|
|
837
907
|
purged: false,
|
|
@@ -917,8 +987,21 @@ export function forgetRegistryRoot(root) {
|
|
|
917
987
|
*/
|
|
918
988
|
const COPY_SKIP_DIRS = new Set(['node_modules', '.git', '.astro', 'dist']);
|
|
919
989
|
|
|
920
|
-
/**
|
|
921
|
-
|
|
990
|
+
/**
|
|
991
|
+
* 复制空间时额外跳过的「派生缓存」(相对空间根,posix 路径)。
|
|
992
|
+
*
|
|
993
|
+
* 这些都能从源数据重建,且复制时若正好被服务进程写着,拷过来只可能是半份 / 不一致的:
|
|
994
|
+
* 会话检索索引用 sqlite(可能带 WAL / shm),spill 只是工具结果的溢出缓存。
|
|
995
|
+
*/
|
|
996
|
+
const COPY_SKIP_LOCAL_FILES = new Set([
|
|
997
|
+
`${LOCAL_REL}/session-query.sqlite`,
|
|
998
|
+
`${LOCAL_REL}/session-query.sqlite-wal`,
|
|
999
|
+
`${LOCAL_REL}/session-query.sqlite-shm`,
|
|
1000
|
+
]);
|
|
1001
|
+
const COPY_SKIP_LOCAL_DIRS = new Set([`${LOCAL_REL}/spill`]);
|
|
1002
|
+
|
|
1003
|
+
/** 递归复制目录(跳过 .DS_Store、构建/版本控制目录与派生缓存,保留符号链接本身而非解引用) */
|
|
1004
|
+
export function copyDirSync(from, to, rel = '') {
|
|
922
1005
|
const src = path.resolve(from);
|
|
923
1006
|
const dest = path.resolve(to);
|
|
924
1007
|
|
|
@@ -931,14 +1014,17 @@ export function copyDirSync(from, to) {
|
|
|
931
1014
|
for (const ent of fs.readdirSync(src, { withFileTypes: true })) {
|
|
932
1015
|
if (ent.name === '.DS_Store') continue;
|
|
933
1016
|
if (ent.isDirectory() && COPY_SKIP_DIRS.has(ent.name)) continue;
|
|
1017
|
+
const nextRel = rel ? `${rel}/${ent.name}` : ent.name;
|
|
934
1018
|
const s = path.join(src, ent.name);
|
|
935
1019
|
const d = path.join(dest, ent.name);
|
|
936
1020
|
if (ent.isSymbolicLink()) {
|
|
937
1021
|
const link = fs.readlinkSync(s);
|
|
938
1022
|
fs.symlinkSync(link, d);
|
|
939
1023
|
} else if (ent.isDirectory()) {
|
|
940
|
-
|
|
1024
|
+
if (COPY_SKIP_LOCAL_DIRS.has(nextRel)) continue;
|
|
1025
|
+
copyDirSync(s, d, nextRel);
|
|
941
1026
|
} else {
|
|
1027
|
+
if (COPY_SKIP_LOCAL_FILES.has(nextRel)) continue;
|
|
942
1028
|
fs.copyFileSync(s, d);
|
|
943
1029
|
}
|
|
944
1030
|
}
|
package/config/app.defaults.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"plugins": {
|
|
17
17
|
"cases": false,
|
|
18
18
|
"caseGen": false,
|
|
19
|
-
"prompts":
|
|
19
|
+
"prompts": true,
|
|
20
20
|
"promptGen": false,
|
|
21
21
|
"knowledgeLib": false,
|
|
22
22
|
"knowledgeGen": false,
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"templates": false,
|
|
25
25
|
"knowledge": false,
|
|
26
26
|
"memory": false,
|
|
27
|
-
"pluginDev": false
|
|
27
|
+
"pluginDev": false,
|
|
28
|
+
"todos": false,
|
|
29
|
+
"tasks": false
|
|
28
30
|
}
|
|
29
31
|
}
|