lantern-harness 0.1.1 → 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 +74 -0
- package/README.md +10 -10
- package/builtin/session/lh-spaces.mjs +105 -0
- package/cli/acp-server.mjs +15 -1
- package/cli/data-root.mjs +80 -3
- package/cli/lantern.mjs +27 -10
- package/cli/server.mjs +4 -0
- package/cli/spaces.mjs +278 -38
- 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.DD-XOFEX.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 +80 -6
- package/dist/docs/install/index.html +26 -12
- 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 +14 -12
- 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,77 @@
|
|
|
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
|
+
|
|
63
|
+
## [0.1.2] - 2026-10-18
|
|
64
|
+
|
|
65
|
+
### 变更
|
|
66
|
+
|
|
67
|
+
- 升级命令由 `lantern install [版本]` 改为 `lantern update` / `lantern update <版本>`,帮助与文档同步;`lantern install` 保留为兼容别名
|
|
68
|
+
- ACP `initialize` 的版本号改为读 `package.json`,不再随发版手改
|
|
69
|
+
|
|
70
|
+
### 修复
|
|
71
|
+
|
|
72
|
+
- 重新 `lantern init` 换了数据目录后,空间列表与「当前空间」仍指向上一个数据目录里的空间:注册表改为按池根分区(每条空间记 `root`,激活项记 `activeByRoot`),列表只看当前数据目录;旧版全局 `activeId` 与无 `root` 的条目在读取时按路径推断归属,向后兼容
|
|
73
|
+
- `lantern web` / `lantern status` 把生效的空间目录标成「当前空间」,不再和数据目录混为一谈
|
|
74
|
+
|
|
3
75
|
## [0.1.1] - 2026-10-18
|
|
4
76
|
|
|
5
77
|
### 变更
|
|
@@ -151,5 +223,7 @@ Lantern Harness 首个版本:AI 工作台。一份进程里隔离多套项目
|
|
|
151
223
|
- 开发服忽略打开内容库目录与空间元数据目录导致的整页热更新
|
|
152
224
|
- 文档与 CLI 统一 pnpm 优先:安装 / 卸载 / 版本列表优先调用 pnpm(未安装则回退 npm);README、安装手册、操作手册与贡献约定的开发命令改为 pnpm
|
|
153
225
|
|
|
226
|
+
[0.1.3]: https://www.npmjs.com/package/lantern-harness/v/0.1.3
|
|
227
|
+
[0.1.2]: https://www.npmjs.com/package/lantern-harness/v/0.1.2
|
|
154
228
|
[0.1.1]: https://www.npmjs.com/package/lantern-harness/v/0.1.1
|
|
155
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
|
|
|
@@ -28,7 +28,7 @@ AI 工作台:对话与插件工具、需求 / 用例 / 知识 / 提示词内
|
|
|
28
28
|
npx lantern-harness
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
执行后会安装全局短命令 `lantern`,之后用 `lantern init` / `lantern web` 操作即可。升级用 `lantern
|
|
31
|
+
执行后会安装全局短命令 `lantern`,之后用 `lantern init` / `lantern web` 操作即可。升级用 `lantern update`。
|
|
32
32
|
|
|
33
33
|
2、 初始化数据空间(首次必做;可再次执行以更换数据空间):
|
|
34
34
|
|
|
@@ -61,9 +61,9 @@ lantern logs # 查看服务日志
|
|
|
61
61
|
lantern doctor # 检查运行环境与配置是否正常
|
|
62
62
|
lantern help # 显示操作命令列表
|
|
63
63
|
lantern version # 显示当前版本号
|
|
64
|
-
lantern list #
|
|
65
|
-
lantern
|
|
66
|
-
lantern
|
|
64
|
+
lantern list # 显示可用版本列表
|
|
65
|
+
lantern update # 更新到最新版本
|
|
66
|
+
lantern update 0.1.x # 更新到指定版本
|
|
67
67
|
lantern uninstall # 卸载本机 lantern 命令(保留数据空间)
|
|
68
68
|
lantern wipe # 删除当前数据空间(需确认;不可恢复)
|
|
69
69
|
lantern space list # 列出全部数据空间与当前项
|
|
@@ -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 {
|
|
@@ -37,6 +41,7 @@ import {
|
|
|
37
41
|
resolveSpace,
|
|
38
42
|
sanitizeSpaceName,
|
|
39
43
|
setActiveSpace,
|
|
44
|
+
setRegistryRoot,
|
|
40
45
|
spaceSize,
|
|
41
46
|
} from '../../cli/spaces.mjs';
|
|
42
47
|
import { loadRunState, saveRunState } from '../../cli/user-config.mjs';
|
|
@@ -206,12 +211,29 @@ function syncActiveRoot() {
|
|
|
206
211
|
return { ok: true, id: activeId, changed: true };
|
|
207
212
|
}
|
|
208
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
|
+
|
|
209
229
|
export function lhSpacesPlugin() {
|
|
210
230
|
return {
|
|
211
231
|
name: 'lh-spaces',
|
|
212
232
|
configureServer(server) {
|
|
213
233
|
// 首次进入确定基准根;已切换过的空间不会被覆盖
|
|
214
234
|
ensureSpaceContext(server.config.root);
|
|
235
|
+
// 注册表按池根分区:先告诉它当前是哪个数据目录,才能只认自己的空间与激活项
|
|
236
|
+
setRegistryRoot(getBaseRoot());
|
|
215
237
|
// 新进程里 activeRoot 默认是公共空间:按注册表回到用户上次选的空间
|
|
216
238
|
syncActiveRoot();
|
|
217
239
|
|
|
@@ -254,6 +276,8 @@ export function lhSpacesPlugin() {
|
|
|
254
276
|
try {
|
|
255
277
|
prepareSpaceDir(created.space.path);
|
|
256
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);
|
|
257
281
|
} catch (e) {
|
|
258
282
|
return sendJson(res, 500, {
|
|
259
283
|
ok: false,
|
|
@@ -277,11 +301,92 @@ export function lhSpacesPlugin() {
|
|
|
277
301
|
const body = await readJson(req);
|
|
278
302
|
const out = renameSpace(body.id, body.name, getBaseRoot());
|
|
279
303
|
if (!out.ok) return sendJson(res, 400, { ok: false, error: out.error });
|
|
304
|
+
retargetActiveRoot(out.space);
|
|
280
305
|
return sendJson(res, 200, { ok: true, space: out.space });
|
|
281
306
|
}
|
|
282
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
|
+
|
|
283
381
|
if (url === `${PREFIX}/copy` && req.method === 'POST') {
|
|
284
382
|
const body = await readJson(req);
|
|
383
|
+
// 复制是逐文件拷贝:会话在跑时源空间仍在写,拷出来可能半份
|
|
384
|
+
if (hasRunningSessions()) {
|
|
385
|
+
return sendJson(res, 409, {
|
|
386
|
+
ok: false,
|
|
387
|
+
error: '仍有会话在运行,请先停止后再复制空间',
|
|
388
|
+
});
|
|
389
|
+
}
|
|
285
390
|
const out = copySpace(body.id, body.name, getBaseRoot());
|
|
286
391
|
if (!out.ok) return sendJson(res, 400, { ok: false, error: out.error });
|
|
287
392
|
return sendJson(res, 200, { ok: true, space: out.space });
|
package/cli/acp-server.mjs
CHANGED
|
@@ -13,10 +13,24 @@
|
|
|
13
13
|
* - session/update { sessionId, toolCall: { name, status, args?, result? } }
|
|
14
14
|
*/
|
|
15
15
|
import readline from 'node:readline';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
16
20
|
import { headlessAsk, loadHeadlessSettings } from './headless.mjs';
|
|
17
21
|
|
|
18
22
|
const PROTOCOL_VERSION = 1;
|
|
19
23
|
|
|
24
|
+
/** 与 package.json 保持同源,避免发版时漏改 */
|
|
25
|
+
const SERVER_VERSION = (() => {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
28
|
+
return JSON.parse(fs.readFileSync(pkg, 'utf8')).version || '0.0.0';
|
|
29
|
+
} catch {
|
|
30
|
+
return '0.0.0';
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
|
|
20
34
|
function send(msg) {
|
|
21
35
|
process.stdout.write(`${JSON.stringify(msg)}\n`);
|
|
22
36
|
}
|
|
@@ -74,7 +88,7 @@ export async function runAcpServer(opts) {
|
|
|
74
88
|
protocolVersion: PROTOCOL_VERSION,
|
|
75
89
|
serverInfo: {
|
|
76
90
|
name: 'lantern-harness-acp',
|
|
77
|
-
version:
|
|
91
|
+
version: SERVER_VERSION,
|
|
78
92
|
},
|
|
79
93
|
capabilities: {
|
|
80
94
|
sessions: true,
|
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
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
createSpace,
|
|
37
37
|
discoverSpaces,
|
|
38
38
|
exportSpaceToZip,
|
|
39
|
+
forgetRegistryRoot,
|
|
39
40
|
importSpaceFromZip,
|
|
40
41
|
listSpaces,
|
|
41
42
|
PUBLIC_SPACE_LABEL,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
renameSpace,
|
|
44
45
|
resolveSpace,
|
|
45
46
|
setActiveSpace,
|
|
47
|
+
setRegistryRoot,
|
|
46
48
|
} from './spaces.mjs';
|
|
47
49
|
|
|
48
50
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -143,8 +145,9 @@ function printHelp() {
|
|
|
143
145
|
lantern doctor 检查运行环境与配置
|
|
144
146
|
lantern help 显示本帮助
|
|
145
147
|
lantern version 显示当前版本号
|
|
146
|
-
lantern list
|
|
147
|
-
lantern
|
|
148
|
+
lantern list 显示可用版本列表
|
|
149
|
+
lantern update 更新到最新版本
|
|
150
|
+
lantern update <版本> 更新到指定版本
|
|
148
151
|
lantern uninstall 卸载本机 lantern(保留数据空间)
|
|
149
152
|
lantern wipe 删除当前数据空间(需确认)
|
|
150
153
|
|
|
@@ -205,6 +208,7 @@ function requireDataRoot(dirOverride) {
|
|
|
205
208
|
if (dirOverride) {
|
|
206
209
|
const dataRoot = path.resolve(dirOverride);
|
|
207
210
|
if (!fs.existsSync(dataRoot)) fs.mkdirSync(dataRoot, { recursive: true });
|
|
211
|
+
setRegistryRoot(dataRoot);
|
|
208
212
|
return dataRoot;
|
|
209
213
|
}
|
|
210
214
|
const result = resolveConfiguredDataRoot();
|
|
@@ -212,6 +216,8 @@ function requireDataRoot(dirOverride) {
|
|
|
212
216
|
printDataRootUnavailable(result);
|
|
213
217
|
process.exit(1);
|
|
214
218
|
}
|
|
219
|
+
// 注册表按池根分区:换过数据空间后,只认这个目录下的空间
|
|
220
|
+
setRegistryRoot(result.dataRoot);
|
|
215
221
|
return result.dataRoot;
|
|
216
222
|
}
|
|
217
223
|
|
|
@@ -239,6 +245,8 @@ async function cmdInit() {
|
|
|
239
245
|
}
|
|
240
246
|
prepareSpacesRoot(spacesRoot);
|
|
241
247
|
saveUserConfig({ dataRoot: spacesRoot, host: '127.0.0.1', port: 4321 });
|
|
248
|
+
// 换数据目录后注册表要按新池根分区,别把上一个目录的空间与激活项带过来
|
|
249
|
+
setRegistryRoot(spacesRoot);
|
|
242
250
|
console.log('');
|
|
243
251
|
console.log('数据目录已关联:');
|
|
244
252
|
console.log(` ${spacesRoot}`);
|
|
@@ -267,7 +275,7 @@ async function runForegroundServer(dataRoot) {
|
|
|
267
275
|
console.log('');
|
|
268
276
|
console.log('Lantern Harness 已启动');
|
|
269
277
|
console.log(` 地址:${url}`);
|
|
270
|
-
console.log(`
|
|
278
|
+
console.log(` 当前空间:${root}`);
|
|
271
279
|
console.log(' 按 Ctrl+C 停止');
|
|
272
280
|
console.log('');
|
|
273
281
|
}
|
|
@@ -278,7 +286,7 @@ function cmdStart({ foreground = false, dir = null } = {}) {
|
|
|
278
286
|
if (alive) {
|
|
279
287
|
console.log(`服务已在运行(pid ${alive.pid})`);
|
|
280
288
|
console.log(` 地址:${alive.url}`);
|
|
281
|
-
console.log(`
|
|
289
|
+
console.log(` 当前空间:${alive.dataRoot}`);
|
|
282
290
|
return;
|
|
283
291
|
}
|
|
284
292
|
|
|
@@ -311,9 +319,10 @@ function cmdStart({ foreground = false, dir = null } = {}) {
|
|
|
311
319
|
console.log('');
|
|
312
320
|
console.log('Lantern Harness 已在后台启动');
|
|
313
321
|
console.log(` 地址:${state.url}`);
|
|
314
|
-
console.log(`
|
|
315
|
-
console.log(` 日志:${logPath}`);
|
|
322
|
+
console.log(` 当前空间:${state.dataRoot}`);
|
|
316
323
|
console.log(' 停止:lantern stop');
|
|
324
|
+
console.log(' 更新:lantern update');
|
|
325
|
+
console.log(' 更多命令:lantern help');
|
|
317
326
|
console.log('');
|
|
318
327
|
return;
|
|
319
328
|
}
|
|
@@ -388,6 +397,7 @@ function cmdStatus() {
|
|
|
388
397
|
if (run) {
|
|
389
398
|
console.log(`状态:运行中(pid ${run.pid})`);
|
|
390
399
|
console.log(`地址:${run.url}`);
|
|
400
|
+
console.log(`当前空间:${run.dataRoot}`);
|
|
391
401
|
} else {
|
|
392
402
|
console.log('状态:未运行');
|
|
393
403
|
}
|
|
@@ -510,10 +520,10 @@ function cmdList() {
|
|
|
510
520
|
}
|
|
511
521
|
}
|
|
512
522
|
|
|
513
|
-
function
|
|
523
|
+
function cmdUpdate(version) {
|
|
514
524
|
const pm = getPackageManager();
|
|
515
525
|
const spec = version ? `${PKG_NAME}@${version}` : `${PKG_NAME}@latest`;
|
|
516
|
-
console.log(
|
|
526
|
+
console.log(version ? `正在更新到 ${spec} …` : `正在更新到最新版本(${spec})…`);
|
|
517
527
|
const r = spawnSync(pm, pmGlobalArgs(pm, 'install', spec), {
|
|
518
528
|
stdio: 'inherit',
|
|
519
529
|
shell: process.platform === 'win32',
|
|
@@ -526,8 +536,12 @@ function cmdInstall(version) {
|
|
|
526
536
|
/** CLI 里的「基准根」:与网页端一致 —— 已记住的数据空间 */
|
|
527
537
|
function spaceBaseRoot() {
|
|
528
538
|
const result = resolveConfiguredDataRoot();
|
|
529
|
-
if (result.ok)
|
|
539
|
+
if (result.ok) {
|
|
540
|
+
setRegistryRoot(result.dataRoot);
|
|
541
|
+
return result.dataRoot;
|
|
542
|
+
}
|
|
530
543
|
// 尚未 init 时,池根退化到当前目录,保证 space 命令本身可用
|
|
544
|
+
setRegistryRoot(process.cwd());
|
|
531
545
|
return process.cwd();
|
|
532
546
|
}
|
|
533
547
|
|
|
@@ -823,6 +837,7 @@ async function cmdWipe() {
|
|
|
823
837
|
if (run) cmdStop();
|
|
824
838
|
|
|
825
839
|
fs.rmSync(resolved, { recursive: true, force: true });
|
|
840
|
+
forgetRegistryRoot(resolved);
|
|
826
841
|
clearDataRootFromConfig();
|
|
827
842
|
console.log('数据空间已删除,配置中的关联已清除。');
|
|
828
843
|
console.log('如需继续使用,请使用 lantern init 初始化或选择已有数据空间。');
|
|
@@ -933,8 +948,10 @@ async function main() {
|
|
|
933
948
|
case 'list':
|
|
934
949
|
cmdList();
|
|
935
950
|
break;
|
|
951
|
+
case 'update':
|
|
952
|
+
// 旧文档 / 老脚本里的 install 仍可用,输出与 update 一致
|
|
936
953
|
case 'install':
|
|
937
|
-
|
|
954
|
+
cmdUpdate(args[0]);
|
|
938
955
|
break;
|
|
939
956
|
case 'uninstall':
|
|
940
957
|
cmdUninstall();
|
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(),
|