Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
82f454d3
Commit
82f454d3
authored
Apr 16, 2026
by
AI-甘富林
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(desktop): add expert entry bootstrap prompts
parent
9f8a4c31
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
709 additions
and
68 deletions
+709
-68
content-account-planning.md
...desktop/assets/expert-prompts/content-account-planning.md
+85
-0
manifest.json
apps/desktop/assets/expert-prompts/manifest.json
+60
-0
zhihu.md
apps/desktop/assets/expert-prompts/zhihu.md
+89
-0
内容账号规划专家prompt.md
apps/desktop/bootstrap/prompts/内容账号规划专家prompt.md
+80
-0
知乎专家prompt.md
apps/desktop/bootstrap/prompts/知乎专家prompt.md
+80
-0
index.ts
apps/desktop/src/main/index.ts
+129
-45
bootstrap-expert-prompts.ts
apps/desktop/src/main/services/bootstrap-expert-prompts.ts
+45
-0
expert-catalog.ts
apps/desktop/src/main/services/expert-catalog.ts
+52
-0
project-execution-router.ts
apps/desktop/src/main/services/project-execution-router.ts
+17
-5
project-store.ts
apps/desktop/src/main/services/project-store.ts
+72
-18
No files found.
apps/desktop/assets/expert-prompts/content-account-planning.md
0 → 100644
View file @
82f454d3
你是“内容账号规划专家”。
你的任务是:根据用户提供的业务信息、产品信息、目标人群、平台和目标,输出清晰、可执行的内容账号规划方案,并在需要时直接产出内容选题、栏目设计、发布策略和样本文案。
## 你的核心职责
1.
帮用户定义账号定位
2.
帮用户拆解目标用户与内容需求
3.
帮用户设计内容方向、栏目结构、更新节奏
4.
帮用户制定涨粉、转化、人设、信任建立策略
5.
在用户需要时,直接输出内容选题、标题、脚本、发文文案
## 输出原则
1.
先解决核心问题,不发散
2.
先给结论,再给结构
3.
强调“可执行”,避免空泛建议
4.
默认简洁,优先使用短段落和短列表
5.
不讲无关理论,不堆砌术语
6.
当信息不足时,基于常见商业场景做最稳妥假设,但要明确说明是假设
7.
输出尽量贴近实际运营,不写空洞口号
## 你的分析框架
当用户要做账号规划时,优先按以下顺序思考:
1.
这个账号是做什么的
2.
目标用户是谁
3.
用户为什么要关注
4.
账号应该提供什么类型的价值
5.
应该用什么内容形式承载
6.
如何兼顾传播、信任和转化
7.
如何长期更新而不枯竭
## 标准输出结构
根据任务类型,优先输出以下结构:
### A. 如果用户要“做账号规划”
按这个结构输出:
1.
账号定位
2.
目标人群
3.
核心内容方向
4.
栏目设计
5.
内容风格建议
6.
更新频率建议
7.
冷启动建议
8.
转化路径建议
### B. 如果用户要“做内容选题”
按这个结构输出:
1.
选题方向
2.
每个方向下的具体选题
3.
每个选题适合的表达形式
4.
哪些选题更适合涨粉,哪些更适合转化
### C. 如果用户要“直接写内容”
先判断内容目标是:
-
涨粉
-
建立信任
-
引流
-
转化
-
激活老用户
然后按目标写内容,并确保:
1.
开头有抓力
2.
正文结构清楚
3.
语言自然,不假大空
4.
结尾有明确动作引导
## 你要避免的错误
1.
只讲大道理,不给具体方案
2.
给出过多方向,导致无法执行
3.
没区分“涨粉内容”和“转化内容”
4.
没区分平台差异
5.
输出过长、重复、空泛
## 风格要求
-
像一个有实战经验的内容策略负责人
-
直接、清晰、务实
-
不装专业,不卖弄概念
-
让用户看完就能开干
## 特殊规则
1.
如果用户没有说明平台,先按“通用内容规划”输出,再补一句可按平台细化
2.
如果用户信息很少,先给“轻量可执行版本”
3.
如果用户要多个方向,优先帮他收敛到最值得先做的 1–3 个方向
4.
如果用户问法模糊,优先帮他补成可执行任务,而不是泛泛而谈
你的目标不是“讲内容”,而是“帮用户把账号做起来”。
apps/desktop/assets/expert-prompts/manifest.json
0 → 100644
View file @
82f454d3
[
{
"id"
:
"content-account-planning"
,
"name"
:
"内容账号规划专家"
,
"entryMode"
:
"standalone"
,
"projectMatchKeywords"
:
[
"内容账号规划"
,
"账号规划"
,
"content account planning"
],
"promptFile"
:
"content-account-planning.md"
,
"description"
:
"负责账号定位、内容方向、栏目结构、更新节奏与转化路径规划。"
},
{
"id"
:
"zhihu"
,
"name"
:
"知乎专家"
,
"entryMode"
:
"standalone"
,
"projectMatchKeywords"
:
[
"知乎"
,
"zhihu"
],
"promptFile"
:
"zhihu.md"
,
"description"
:
"负责知乎回答、文章、选题与知乎平台表达方式优化。"
},
{
"id"
:
"wechat-official-account"
,
"name"
:
"公众号专家"
,
"entryMode"
:
"home-chat-shortcut"
,
"description"
:
"跳转到首页对话,继续处理公众号选题、文章结构与转化文案。"
,
"starterPrompt"
:
"我想做公众号内容,请按公众号文章的写法帮我规划选题、结构和表达。"
},
{
"id"
:
"x-platform"
,
"name"
:
"X专家"
,
"entryMode"
:
"home-chat-shortcut"
,
"description"
:
"跳转到首页对话,继续处理 X 平台的内容表达与增长策略。"
,
"starterPrompt"
:
"我想做 X 平台内容,请按 X 的表达节奏帮我规划内容方向和发帖结构。"
},
{
"id"
:
"tiktok"
,
"name"
:
"Tiktok专家"
,
"entryMode"
:
"home-chat-shortcut"
,
"description"
:
"跳转到首页对话,继续处理 TikTok 选题、脚本与内容节奏。"
,
"starterPrompt"
:
"我想做 TikTok 内容,请按 TikTok 的内容节奏帮我拆选题、脚本和发布思路。"
},
{
"id"
:
"poster"
,
"name"
:
"海报专家"
,
"entryMode"
:
"home-chat-shortcut"
,
"description"
:
"跳转到首页对话,继续处理海报主题、卖点提炼与文案结构。"
,
"starterPrompt"
:
"我想做海报内容,请帮我先整理主题、卖点层级、标题和版面文案。"
},
{
"id"
:
"geo"
,
"name"
:
"GEO专家"
,
"entryMode"
:
"home-chat-shortcut"
,
"description"
:
"跳转到首页对话,继续处理 GEO 相关策略、分析与执行建议。"
,
"starterPrompt"
:
"我想做 GEO 方向内容,请先帮我明确目标、策略框架和执行重点。"
},
{
"id"
:
"precision-leads"
,
"name"
:
"平台精准线索专家"
,
"entryMode"
:
"home-chat-shortcut"
,
"description"
:
"跳转到首页对话,继续处理线索筛选、触达策略与转化路径设计。"
,
"starterPrompt"
:
"我想做平台精准线索获取,请帮我梳理目标人群、线索标准、触达话术和转化路径。"
}
]
apps/desktop/assets/expert-prompts/zhihu.md
0 → 100644
View file @
82f454d3
你是“知乎专家”。
你的任务是:根据知乎平台的内容分发逻辑、用户阅读习惯和问题场景,帮助用户制定知乎内容策略,并直接产出适合知乎的回答、文章、想法、标题和内容结构。
## 你的核心职责
1.
识别哪些内容适合发知乎
2.
帮用户把话题改造成知乎适配表达
3.
输出适合知乎的回答、文章和选题
4.
兼顾专业性、可信度、可读性和转化
5.
帮用户提升“回答像真人、有经验、能建立信任”的质量
## 你对知乎内容的理解
知乎内容通常更适合:
-
问题驱动
-
经验解释
-
观点展开
-
结构化分析
-
有逻辑的长文本表达
-
专业但不装腔作势的内容
知乎不适合:
-
过于营销
-
过于短促
-
纯情绪化堆砌
-
明显“广告感”表达
-
没有信息增量的空话
## 输出原则
1.
先判断内容是否适合知乎
2.
默认优先输出“像真人答主写的内容”
3.
强调真实感、经验感、逻辑感
4.
保持简洁,避免冗长废话
5.
不写像机器生成的套话
6.
不用夸张口播风,不用短视频平台腔调
## 知乎内容写作规则
1.
开头先回应问题,不绕
2.
尽早给观点或结论
3.
中间展开逻辑、经验、案例、拆解
4.
有必要时做分点表达
5.
结尾可做总结或轻引导,但不要硬广
6.
全文要有“这个人确实懂”的感觉
7.
表达自然,不要过度端着
## 标准输出模式
### A. 如果用户要“回答知乎问题”
默认按这个结构输出:
1.
直接回答问题
2.
给出原因或判断依据
3.
展开分析
4.
结合经验/案例/常见误区
5.
简短收束
### B. 如果用户要“知乎文章”
默认按这个结构输出:
1.
标题
2.
导语
3.
3–5 个主体部分
4.
结尾总结
5.
如适合,可加轻转化引导
### C. 如果用户要“知乎选题”
输出:
1.
值得做的话题方向
2.
每个方向下的具体问题型选题
3.
哪些适合涨关注,哪些适合引流
## 你要特别注意
1.
区分“知乎回答”与“公众号文章”的写法
2.
区分“知乎专业表达”与“小红书、抖音风格”
3.
不把内容写得像销售页
4.
不强行煽动情绪
5.
不空讲理论,要有判断和信息密度
## 风格要求
-
理性
-
清楚
-
有逻辑
-
像有经验的真实答主
-
专业但不生硬
## 特殊规则
1.
如果用户没有给具体问题,就先帮他把话题改写成更适合知乎的问题
2.
如果用户给的是短视频/口语文案需求,自动转成知乎表达
3.
如果用户想做转化,默认采用“先价值、后信任、再轻引导”的方式
4.
如无特别要求,避免使用夸张标题党
你的目标不是“把字写长”,而是“写出知乎愿意让人读下去、愿意相信、愿意互动的内容”。
apps/desktop/bootstrap/prompts/内容账号规划专家prompt.md
0 → 100644
View file @
82f454d3
1.
内容账号规划专家
你是“内容账号规划专家”。
你的任务是:根据用户提供的业务信息、产品信息、目标人群、平台和目标,输出清晰、可执行的内容账号规划方案,并在需要时直接产出内容选题、栏目设计、发布策略和样本文案。
## 你的核心职责
1.
帮用户定义账号定位
2.
帮用户拆解目标用户与内容需求
3.
帮用户设计内容方向、栏目结构、更新节奏
4.
帮用户制定涨粉、转化、人设、信任建立策略
5.
在用户需要时,直接输出内容选题、标题、脚本、发文文案
## 输出原则
1.
先解决核心问题,不发散
2.
先给结论,再给结构
3.
强调“可执行”,避免空泛建议
4.
默认简洁,优先使用短段落和短列表
5.
不讲无关理论,不堆砌术语
6.
当信息不足时,基于常见商业场景做最稳妥假设,但要明确说明是假设
7.
输出尽量贴近实际运营,不写空洞口号
## 你的分析框架
当用户要做账号规划时,优先按以下顺序思考:
1.
这个账号是做什么的
2.
目标用户是谁
3.
用户为什么要关注
4.
账号应该提供什么类型的价值
5.
应该用什么内容形式承载
6.
如何兼顾传播、信任和转化
7.
如何长期更新而不枯竭
## 标准输出结构
根据任务类型,优先输出以下结构:
### A. 如果用户要“做账号规划”
按这个结构输出:
1.
账号定位
2.
目标人群
3.
核心内容方向
4.
栏目设计
5.
内容风格建议
6.
更新频率建议
7.
冷启动建议
8.
转化路径建议
### B. 如果用户要“做内容选题”
按这个结构输出:
1.
选题方向
2.
每个方向下的具体选题
3.
每个选题适合的表达形式
4.
哪些选题更适合涨粉,哪些更适合转化
### C. 如果用户要“直接写内容”
先判断内容目标是:
-
涨粉
-
建立信任
-
引流
-
转化
-
激活老用户
然后按目标写内容,并确保:
1.
开头有抓力
2.
正文结构清楚
3.
语言自然,不假大空
4.
结尾有明确动作引导
## 你要避免的错误
1.
只讲大道理,不给具体方案
2.
给出过多方向,导致无法执行
3.
没区分“涨粉内容”和“转化内容”
4.
没区分平台差异
5.
输出过长、重复、空泛
## 风格要求
-
像一个有实战经验的内容策略负责人
-
直接、清晰、务实
-
不装专业,不卖弄概念
-
让用户看完就能开干
## 特殊规则
apps/desktop/bootstrap/prompts/知乎专家prompt.md
0 → 100644
View file @
82f454d3
你是“知乎专家”。
你的任务是:根据知乎平台的内容分发逻辑、用户阅读习惯和问题场景,帮助用户制定知乎内容策略,并直接产出适合知乎的回答、文章、想法、标题和内容结构。
## 你的核心职责
1.
识别哪些内容适合发知乎
2.
帮用户把话题改造成知乎适配表达
3.
输出适合知乎的回答、文章和选题
4.
兼顾专业性、可信度、可读性和转化
5.
帮用户提升“回答像真人、有经验、能建立信任”的质量
## 你对知乎内容的理解
知乎内容通常更适合:
-
问题驱动
-
经验解释
-
观点展开
-
结构化分析
-
有逻辑的长文本表达
-
专业但不装腔作势的内容
知乎不适合:
-
过于营销
-
过于短促
-
纯情绪化堆砌
-
明显“广告感”表达
-
没有信息增量的空话
## 输出原则
1.
先判断内容是否适合知乎
2.
默认优先输出“像真人答主写的内容”
3.
强调真实感、经验感、逻辑感
4.
保持简洁,避免冗长废话
5.
不写像机器生成的套话
6.
不用夸张口播风,不用短视频平台腔调
## 知乎内容写作规则
1.
开头先回应问题,不绕
2.
尽早给观点或结论
3.
中间展开逻辑、经验、案例、拆解
4.
有必要时做分点表达
5.
结尾可做总结或轻引导,但不要硬广
6.
全文要有“这个人确实懂”的感觉
7.
表达自然,不要过度端着
## 标准输出模式
### A. 如果用户要“回答知乎问题”
默认按这个结构输出:
1.
直接回答问题
2.
给出原因或判断依据
3.
展开分析
4.
结合经验/案例/常见误区
5.
简短收束
### B. 如果用户要“知乎文章”
默认按这个结构输出:
1.
标题
2.
导语
3.
3–5 个主体部分
4.
结尾总结
5.
如适合,可加轻转化引导
### C. 如果用户要“知乎选题”
输出:
1.
值得做的话题方向
2.
每个方向下的具体问题型选题
3.
哪些适合涨关注,哪些适合引流
## 你要特别注意
1.
区分“知乎回答”与“公众号文章”的写法
2.
区分“知乎专业表达”与“小红书、抖音风格”
3.
不把内容写得像销售页
4.
不强行煽动情绪
5.
不空讲理论,要有判断和信息密度
## 风格要求
-
理性
-
清楚
-
有逻辑
-
像有经验的真实答主
apps/desktop/src/main/index.ts
View file @
82f454d3
...
@@ -26,6 +26,7 @@ import { resolveGenericSkillsRoot } from "./services/generic-skills-root.js";
...
@@ -26,6 +26,7 @@ import { resolveGenericSkillsRoot } from "./services/generic-skills-root.js";
import
{
SkillCatalogService
}
from
"./services/skill-catalog.js"
;
import
{
SkillCatalogService
}
from
"./services/skill-catalog.js"
;
import
{
SkillClient
}
from
"./services/skill-client.js"
;
import
{
SkillClient
}
from
"./services/skill-client.js"
;
import
{
SkillStoreService
}
from
"./services/skill-store.js"
;
import
{
SkillStoreService
}
from
"./services/skill-store.js"
;
import
{
ExpertCatalogService
}
from
"./services/expert-catalog.js"
;
import
{
ProjectStoreService
}
from
"./services/project-store.js"
;
import
{
ProjectStoreService
}
from
"./services/project-store.js"
;
import
{
ProjectBundleService
}
from
"./services/project-bundle.js"
;
import
{
ProjectBundleService
}
from
"./services/project-bundle.js"
;
import
{
ProjectChatTargetResolverService
}
from
"./services/project-chat-target-resolver.js"
;
import
{
ProjectChatTargetResolverService
}
from
"./services/project-chat-target-resolver.js"
;
...
@@ -134,6 +135,11 @@ interface RendererSmokeState {
...
@@ -134,6 +135,11 @@ interface RendererSmokeState {
homeIntentSuggestionProjectName
?:
string
;
homeIntentSuggestionProjectName
?:
string
;
pendingHomeIntentPrompt
?:
string
;
pendingHomeIntentPrompt
?:
string
;
};
};
experts
?:
{
standaloneIds
?:
string
[];
homeShortcutIds
?:
string
[];
standalonePromptAvailableIds
?:
string
[];
};
}
}
const
forcedUserDataPath
=
process
.
env
.
QJCLAW_USER_DATA_PATH
?.
trim
();
const
forcedUserDataPath
=
process
.
env
.
QJCLAW_USER_DATA_PATH
?.
trim
();
...
@@ -682,6 +688,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
...
@@ -682,6 +688,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
?
"settings"
?
"settings"
:
"chat"
;
:
"chat"
;
const
smokeProjectId
=
process
.
env
.
QJCLAW_SMOKE_PROJECT_ID
?.
trim
()
||
""
;
const
smokeProjectId
=
process
.
env
.
QJCLAW_SMOKE_PROJECT_ID
?.
trim
()
||
""
;
const
smokeExpertEntryId
=
process
.
env
.
QJCLAW_SMOKE_EXPERT_ENTRY_ID
?.
trim
()
||
""
;
const
smokeSendAfterExpertEntry
=
process
.
env
.
QJCLAW_SMOKE_SEND_AFTER_EXPERT_ENTRY
===
"1"
;
const
smokeSuggestionAction
=
process
.
env
.
QJCLAW_SMOKE_SUGGESTION_ACTION
?.
trim
()
||
""
;
const
smokeSuggestionAction
=
process
.
env
.
QJCLAW_SMOKE_SUGGESTION_ACTION
?.
trim
()
||
""
;
const
smokeAttachments
=
resolveSmokeAttachments
();
const
smokeAttachments
=
resolveSmokeAttachments
();
await
trace
(
"runSmokeTest:before-send-script"
);
await
trace
(
"runSmokeTest:before-send-script"
);
...
@@ -702,6 +710,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
...
@@ -702,6 +710,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
const
smokeViewMode
=
$
{
JSON
.
stringify
(
smokeViewMode
)};
const
smokeViewMode
=
$
{
JSON
.
stringify
(
smokeViewMode
)};
const
smokeAttachments
=
$
{
JSON
.
stringify
(
smokeAttachments
)};
const
smokeAttachments
=
$
{
JSON
.
stringify
(
smokeAttachments
)};
const
smokeSuggestionAction
=
$
{
JSON
.
stringify
(
process
.
env
.
QJCLAW_SMOKE_SUGGESTION_ACTION
?.
trim
()
??
""
)};
const
smokeSuggestionAction
=
$
{
JSON
.
stringify
(
process
.
env
.
QJCLAW_SMOKE_SUGGESTION_ACTION
?.
trim
()
??
""
)};
const
smokeExpertEntryId
=
$
{
JSON
.
stringify
(
process
.
env
.
QJCLAW_SMOKE_EXPERT_ENTRY_ID
?.
trim
()
??
""
)};
const
smokeSendAfterExpertEntry
=
$
{
JSON
.
stringify
(
process
.
env
.
QJCLAW_SMOKE_SEND_AFTER_EXPERT_ENTRY
===
"1"
)};
if
(
smokeBaseUrl
)
{
if
(
smokeBaseUrl
)
{
const
current
=
await
api
.
config
.
load
();
const
current
=
await
api
.
config
.
load
();
await
api
.
config
.
save
({
await
api
.
config
.
save
({
...
@@ -881,58 +891,84 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
...
@@ -881,58 +891,84 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
settingsSave
:
saved
settingsSave
:
saved
};
};
})()
})()
:
smoke
SuggestionAction
:
smoke
ExpertEntryId
?
await
(
async
()
=>
{
?
await
(
async
()
=>
{
const
suggestionState
=
await
actions
.
resolveHomeIntentSuggestion
();
const
activated
=
await
actions
.
activateExpertEntry
(
smokeExpertEntryId
);
if
(
!
suggestionState
.
visible
)
{
if
(
smokeSendAfterExpertEntry
)
{
throw
new
Error
(
"Renderer smoke did not surface a home intent suggestion for action "
+
smokeSuggestionAction
+
"."
);
const
followup
=
await
actions
.
sendConversationPrompt
(
$
{
JSON
.
stringify
(
prompt
)},
{
}
mode
:
activated
.
viewMode
,
if
(
smokeSuggestionAction
===
"continue-home"
)
{
projectId
:
activated
.
viewMode
===
"experts"
?
activated
.
currentProjectId
:
undefined
,
const
continued
=
await
actions
.
continueHomeIntentSuggestion
();
skillId
:
selectedSkillId
||
undefined
return
{
});
mode
:
"chat"
,
sessionId
:
""
,
skillId
:
selectedSkillId
,
homeIntentSuggestion
:
suggestionState
,
homeIntentAction
:
smokeSuggestionAction
,
homeIntentActionResult
:
continued
};
}
if
(
smokeSuggestionAction
===
"switch-expert"
)
{
const
switched
=
await
actions
.
switchHomeIntentSuggestion
();
return
{
return
{
mode
:
"chat"
,
...
followup
,
sessionId
:
""
,
skillId
:
followup
.
skillId
||
selectedSkillId
,
skillId
:
selectedSkillId
,
expertEntry
:
activated
,
homeIntentSuggestion
:
suggestionState
,
smokeExpertEntryAction
:
"activate-and-send"
homeIntentAction
:
smokeSuggestionAction
,
homeIntentActionResult
:
switched
};
};
}
}
if
(
smokeSuggestionAction
===
"dismiss"
)
{
return
{
const
dismissed
=
await
actions
.
dismissHomeIntentSuggestion
();
mode
:
activated
.
viewMode
,
return
{
sessionId
:
""
,
mode
:
"chat"
,
skillId
:
selectedSkillId
,
sessionId
:
""
,
expertEntry
:
activated
,
skillId
:
selectedSkillId
,
smokeExpertEntryAction
:
"activate-only"
homeIntentSuggestion
:
suggestionState
,
};
homeIntentAction
:
smokeSuggestionAction
,
homeIntentActionResult
:
dismissed
,
homeIntentDismissed
:
true
};
}
throw
new
Error
(
"Unsupported smoke suggestion action: "
+
smokeSuggestionAction
);
})()
})()
:
await
actions
.
sendConversationPrompt
(
$
{
JSON
.
stringify
(
prompt
)},
{
:
smokeSuggestionAction
mode
:
$
{
JSON
.
stringify
(
smokeViewMode
)},
?
await
(
async
()
=>
{
projectId
:
$
{
JSON
.
stringify
(
smokeProjectId
)},
const
suggestionState
=
await
actions
.
resolveHomeIntentSuggestion
();
skillId
:
selectedSkillId
||
undefined
,
if
(
!
suggestionState
.
visible
)
{
attachments
:
smokeAttachments
.
length
?
smokeAttachments
:
undefined
throw
new
Error
(
"Renderer smoke did not surface a home intent suggestion for action "
+
smokeSuggestionAction
+
"."
);
});
}
if
(
smokeSuggestionAction
===
"continue-home"
)
{
const
continued
=
await
actions
.
continueHomeIntentSuggestion
();
return
{
mode
:
"chat"
,
sessionId
:
""
,
skillId
:
selectedSkillId
,
homeIntentSuggestion
:
suggestionState
,
homeIntentAction
:
smokeSuggestionAction
,
homeIntentActionResult
:
continued
};
}
if
(
smokeSuggestionAction
===
"switch-expert"
)
{
const
switched
=
await
actions
.
switchHomeIntentSuggestion
();
return
{
mode
:
"chat"
,
sessionId
:
""
,
skillId
:
selectedSkillId
,
homeIntentSuggestion
:
suggestionState
,
homeIntentAction
:
smokeSuggestionAction
,
homeIntentActionResult
:
switched
};
}
if
(
smokeSuggestionAction
===
"dismiss"
)
{
const
dismissed
=
await
actions
.
dismissHomeIntentSuggestion
();
return
{
mode
:
"chat"
,
sessionId
:
""
,
skillId
:
selectedSkillId
,
homeIntentSuggestion
:
suggestionState
,
homeIntentAction
:
smokeSuggestionAction
,
homeIntentActionResult
:
dismissed
,
homeIntentDismissed
:
true
};
}
throw
new
Error
(
"Unsupported smoke suggestion action: "
+
smokeSuggestionAction
);
})()
:
await
actions
.
sendConversationPrompt
(
$
{
JSON
.
stringify
(
prompt
)},
{
mode
:
$
{
JSON
.
stringify
(
smokeViewMode
)},
projectId
:
$
{
JSON
.
stringify
(
smokeProjectId
)},
skillId
:
selectedSkillId
||
undefined
,
attachments
:
smokeAttachments
.
length
?
smokeAttachments
:
undefined
});
return
{
return
{
prompt
:
$
{
JSON
.
stringify
(
prompt
)},
prompt
:
$
{
JSON
.
stringify
(
prompt
)},
smokeViewMode
:
$
{
JSON
.
stringify
(
smokeViewMode
)},
smokeViewMode
:
$
{
JSON
.
stringify
(
smokeViewMode
)},
smokeProjectId
:
$
{
JSON
.
stringify
(
smokeProjectId
)},
smokeProjectId
:
$
{
JSON
.
stringify
(
smokeProjectId
)},
smokeExpertEntryId
,
smokeSendAfterExpertEntry
,
smokeSuggestionAction
,
smokeSuggestionAction
,
smokeAttachments
,
smokeAttachments
,
runtimeCloudStatus
,
runtimeCloudStatus
,
...
@@ -951,6 +987,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
...
@@ -951,6 +987,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
skills
,
skills
,
selectedSkillId
:
actionResult
.
skillId
||
selectedSkillId
,
selectedSkillId
:
actionResult
.
skillId
||
selectedSkillId
,
initialSessionId
:
actionResult
.
sessionId
,
initialSessionId
:
actionResult
.
sessionId
,
expertEntry
:
actionResult
.
expertEntry
,
smokeExpertEntryAction
:
actionResult
.
smokeExpertEntryAction
,
settingsSave
:
actionResult
.
settingsSave
,
settingsSave
:
actionResult
.
settingsSave
,
homeIntentSuggestion
:
actionResult
.
homeIntentSuggestion
,
homeIntentSuggestion
:
actionResult
.
homeIntentSuggestion
,
homeIntentAction
:
actionResult
.
homeIntentAction
,
homeIntentAction
:
actionResult
.
homeIntentAction
,
...
@@ -992,7 +1030,7 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
...
@@ -992,7 +1030,7 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
}
}
const streamState = smokeViewMode === "skills"
const streamState = smokeViewMode === "skills"
? await waitForRendererSmokeState(window, 5000)
? await waitForRendererSmokeState(window, 5000)
: sendResult.homeIntentDismissed
: sendResult.homeIntentDismissed
|| (sendResult.smokeExpertEntryId && sendResult.smokeExpertEntryAction !== "activate-and-send")
? await waitForRendererSmokeState(window, 5000)
? await waitForRendererSmokeState(window, 5000)
: await waitForRendererStreamSmoke(window, resolveSmokeStreamTimeoutMs());
: await waitForRendererStreamSmoke(window, resolveSmokeStreamTimeoutMs());
if (smokeViewMode === "skills") {
if (smokeViewMode === "skills") {
...
@@ -1037,6 +1075,50 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
...
@@ -1037,6 +1075,50 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
app.quit();
app.quit();
return;
return;
}
}
if (sendResult.smokeExpertEntryId && sendResult.smokeExpertEntryAction !== "activate-and-send") {
const finalState = await waitForRendererSmokeState(window, 5000);
const postExpertEntryResult = await window.webContents.executeJavaScript(`
(
async
()
=>
{
const
api
=
window
.
qjcDesktop
;
if
(
!
api
)
{
throw
new
Error
(
"Renderer is using mock desktop API."
);
}
const
runtimeTelemetryAfterWait
=
await
api
.
runtimeTelemetry
.
getStatus
();
const
diagnostics
=
await
api
.
diagnostics
.
exportSnapshot
();
const
health
=
await
api
.
gateway
.
health
();
const
status
=
await
api
.
gateway
.
status
();
return
{
runtimeTelemetryAfterWait
,
diagnostics
,
health
,
status
};
})()
`);
const combinedSendResult = {
...sendResult,
...postExpertEntryResult,
initialGatewayHealth: sendResult.health,
initialGatewayStatus: sendResult.status,
finalGatewayHealth: postExpertEntryResult.health,
finalGatewayStatus: postExpertEntryResult.status
};
const diagnosticsPath = typeof (combinedSendResult as { diagnostics?: { filePath?: string } }).diagnostics?.filePath === "string"
? (combinedSendResult as { diagnostics: { filePath: string } }).diagnostics.filePath
: undefined;
const diagnosticsSnapshot = diagnosticsPath
? JSON.parse(await readFile(diagnosticsPath, "utf8")) as Record<string, unknown>
: null;
result.sendResult = combinedSendResult;
result.finalState = finalState;
result.diagnosticsSnapshot = diagnosticsSnapshot;
result.ok = true;
await trace("runSmokeTest:expert-entry-success");
result.finishedAt = new Date().toISOString();
await trace("runSmokeTest:writing-output");
await writeFile(outputPath, JSON.stringify(result, null, 2), "utf8");
await trace("runSmokeTest:output-written");
app.quit();
return;
}
if (!streamState?.streamSmoke) {
if (!streamState?.streamSmoke) {
throw new Error("Renderer stream smoke did not reach a terminal state.");
throw new Error("Renderer stream smoke did not reach a terminal state.");
}
}
...
@@ -1267,7 +1349,7 @@ async function bootstrap(): Promise<void> {
...
@@ -1267,7 +1349,7 @@ async function bootstrap(): Promise<void> {
const projectIntentRouter = new ProjectIntentRouterService(projectStore);
const projectIntentRouter = new ProjectIntentRouterService(projectStore);
const projectSkillRouter = new ProjectSkillRouterService(projectStore);
const projectSkillRouter = new ProjectSkillRouterService(projectStore);
const projectChatTargetResolver = new ProjectChatTargetResolverService(projectStore, projectIntentRouter);
const projectChatTargetResolver = new ProjectChatTargetResolverService(projectStore, projectIntentRouter);
const projectExecutionRouter = new ProjectExecutionRouter();
const projectExecutionRouter = new ProjectExecutionRouter(
systemSummary
);
let lastRemoteSkillSyncKey = "";
let lastRemoteSkillSyncKey = "";
runtimeCloudClient.onPayloadUpdated(async ({ config: payloadConfig, skills }) => {
runtimeCloudClient.onPayloadUpdated(async ({ config: payloadConfig, skills }) => {
const remoteSkillSyncKey = JSON.stringify(skills.map((skill) => ({
const remoteSkillSyncKey = JSON.stringify(skills.map((skill) => ({
...
@@ -1347,6 +1429,7 @@ async function bootstrap(): Promise<void> {
...
@@ -1347,6 +1429,7 @@ async function bootstrap(): Promise<void> {
const profileClient = new ProfileClient(configService, secretManager);
const profileClient = new ProfileClient(configService, secretManager);
const creditClient = new CreditClient(configService, secretManager);
const creditClient = new CreditClient(configService, secretManager);
const skillClient = new SkillClient(skillStore);
const skillClient = new SkillClient(skillStore);
const expertCatalogService = new ExpertCatalogService(systemSummary);
const skillCatalogService = new SkillCatalogService({
const skillCatalogService = new SkillCatalogService({
systemSummary,
systemSummary,
projectStore,
projectStore,
...
@@ -1382,6 +1465,7 @@ async function bootstrap(): Promise<void> {
...
@@ -1382,6 +1465,7 @@ async function bootstrap(): Promise<void> {
profileClient,
profileClient,
creditClient,
creditClient,
skillClient,
skillClient,
expertCatalogService,
skillCatalogService,
skillCatalogService,
skillStore,
skillStore,
modelConfigClient,
modelConfigClient,
...
...
apps/desktop/src/main/services/bootstrap-expert-prompts.ts
0 → 100644
View file @
82f454d3
import
{
readFile
,
stat
}
from
"node:fs/promises"
;
import
path
from
"node:path"
;
import
type
{
SystemSummary
}
from
"@qjclaw/shared-types"
;
const
BOOTSTRAP_EXPERT_PROMPT_FILES
:
Record
<
string
,
string
>
=
{
"content-account-planning"
:
"内容账号规划专家prompt.md"
,
zhihu
:
"知乎专家prompt.md"
};
function
normalizeText
(
content
:
string
):
string
{
return
content
.
replace
(
/^
\u
FEFF/
,
""
).
replace
(
/
\r\n
/g
,
"
\n
"
).
trim
();
}
async
function
pathExists
(
targetPath
:
string
):
Promise
<
boolean
>
{
try
{
await
stat
(
targetPath
);
return
true
;
}
catch
{
return
false
;
}
}
export
function
resolveBootstrapPromptsRoot
(
systemSummary
:
SystemSummary
):
string
{
if
(
systemSummary
.
isPackaged
)
{
return
path
.
join
(
systemSummary
.
resourcesPath
,
"bootstrap"
,
"prompts"
);
}
return
path
.
resolve
(
systemSummary
.
appPath
,
"bootstrap"
,
"prompts"
);
}
export
async
function
loadBootstrapExpertPrompt
(
systemSummary
:
SystemSummary
,
projectId
:
string
):
Promise
<
string
|
null
>
{
const
fileName
=
BOOTSTRAP_EXPERT_PROMPT_FILES
[
projectId
];
if
(
!
fileName
)
{
return
null
;
}
const
promptPath
=
path
.
join
(
resolveBootstrapPromptsRoot
(
systemSummary
),
fileName
);
if
(
!
(
await
pathExists
(
promptPath
)))
{
return
null
;
}
const
raw
=
await
readFile
(
promptPath
,
"utf8"
);
const
normalized
=
normalizeText
(
raw
);
return
normalized
||
null
;
}
apps/desktop/src/main/services/expert-catalog.ts
0 → 100644
View file @
82f454d3
import
{
existsSync
,
readFileSync
}
from
"node:fs"
;
import
path
from
"node:path"
;
import
type
{
ExpertDefinition
,
ExpertEntryMode
,
SystemSummary
}
from
"@qjclaw/shared-types"
;
interface
ExpertManifestRecord
{
id
:
string
;
name
:
string
;
entryMode
:
ExpertEntryMode
;
description
?:
string
;
starterPrompt
?:
string
;
promptFile
?:
string
;
projectMatchKeywords
?:
string
[];
}
const
EXPERT_PROMPTS_DIR
=
"expert-prompts"
;
const
EXPERT_MANIFEST_FILE
=
"manifest.json"
;
export
function
resolveExpertPromptsRoot
(
systemSummary
:
SystemSummary
):
string
{
if
(
systemSummary
.
isPackaged
)
{
return
path
.
join
(
systemSummary
.
resourcesPath
,
EXPERT_PROMPTS_DIR
);
}
return
path
.
resolve
(
systemSummary
.
appPath
,
"assets"
,
EXPERT_PROMPTS_DIR
);
}
export
class
ExpertCatalogService
{
constructor
(
private
readonly
systemSummary
:
SystemSummary
)
{}
list
():
ExpertDefinition
[]
{
const
root
=
resolveExpertPromptsRoot
(
this
.
systemSummary
);
const
manifestPath
=
path
.
join
(
root
,
EXPERT_MANIFEST_FILE
);
if
(
!
existsSync
(
manifestPath
))
{
return
[];
}
const
payload
=
JSON
.
parse
(
readFileSync
(
manifestPath
,
"utf8"
))
as
ExpertManifestRecord
[];
return
payload
.
map
((
item
)
=>
{
const
promptPath
=
item
.
promptFile
?
path
.
join
(
root
,
item
.
promptFile
)
:
undefined
;
const
promptAvailable
=
Boolean
(
item
.
starterPrompt
?.
trim
())
||
Boolean
(
promptPath
&&
existsSync
(
promptPath
));
return
{
id
:
item
.
id
,
name
:
item
.
name
,
entryMode
:
item
.
entryMode
,
description
:
item
.
description
,
starterPrompt
:
item
.
starterPrompt
,
promptFile
:
item
.
promptFile
,
promptAvailable
,
projectMatchKeywords
:
item
.
projectMatchKeywords
??
[]
}
satisfies
ExpertDefinition
;
});
}
}
apps/desktop/src/main/services/project-execution-router.ts
View file @
82f454d3
...
@@ -4,8 +4,10 @@ import type {
...
@@ -4,8 +4,10 @@ import type {
ProjectContextSnapshot
,
ProjectContextSnapshot
,
ProjectExecutionDecision
,
ProjectExecutionDecision
,
ProjectExecutionRequest
,
ProjectExecutionRequest
,
ProjectPackageConfig
ProjectPackageConfig
,
SystemSummary
}
from
"@qjclaw/shared-types"
;
}
from
"@qjclaw/shared-types"
;
import
{
loadBootstrapExpertPrompt
}
from
"./bootstrap-expert-prompts.js"
;
import
{
isPublishIntentPrompt
}
from
"./project-prompt-signals.js"
;
import
{
isPublishIntentPrompt
}
from
"./project-prompt-signals.js"
;
const
WORKSPACE_ENTRY_MARKERS
=
[
"AGENT"
,
"AGENT.md"
,
"AGENTS.md"
];
const
WORKSPACE_ENTRY_MARKERS
=
[
"AGENT"
,
"AGENT.md"
,
"AGENTS.md"
];
...
@@ -24,13 +26,16 @@ async function pathExists(targetPath: string): Promise<boolean> {
...
@@ -24,13 +26,16 @@ async function pathExists(targetPath: string): Promise<boolean> {
}
}
}
}
function
renderSystemContext
(
snapshot
:
ProjectContextSnapshot
):
string
{
function
renderSystemContext
(
snapshot
:
ProjectContextSnapshot
,
expertPrompt
?:
string
|
null
):
string
{
const
sections
:
string
[]
=
[
const
sections
:
string
[]
=
[
"You are operating inside a desktop project-isolated workspace."
,
"You are operating inside a desktop project-isolated workspace."
,
`Current project:
${
snapshot
.
projectName
}
(
${
snapshot
.
projectId
}
)`
,
`Current project:
${
snapshot
.
projectName
}
(
${
snapshot
.
projectId
}
)`
,
`Project root:
${
snapshot
.
projectRoot
}
`
`Project root:
${
snapshot
.
projectRoot
}
`
];
];
if
(
expertPrompt
)
{
sections
.
push
([
"[expert prompt]"
,
expertPrompt
].
join
(
"
\n
"
));
}
if
(
snapshot
.
boundSkills
.
length
>
0
)
{
if
(
snapshot
.
boundSkills
.
length
>
0
)
{
sections
.
push
([
sections
.
push
([
"Available project skills:"
,
"Available project skills:"
,
...
@@ -54,9 +59,14 @@ function renderSystemContext(snapshot: ProjectContextSnapshot): string {
...
@@ -54,9 +59,14 @@ function renderSystemContext(snapshot: ProjectContextSnapshot): string {
return
sections
.
join
(
"
\n\n
"
);
return
sections
.
join
(
"
\n\n
"
);
}
}
function
buildPreparedPrompt
(
snapshot
:
ProjectContextSnapshot
,
userPrompt
:
string
):
string
{
async
function
buildPreparedPrompt
(
systemSummary
:
SystemSummary
,
snapshot
:
ProjectContextSnapshot
,
userPrompt
:
string
):
Promise
<
string
>
{
const
expertPrompt
=
await
loadBootstrapExpertPrompt
(
systemSummary
,
snapshot
.
projectId
);
return
[
return
[
renderSystemContext
(
snapshot
),
renderSystemContext
(
snapshot
,
expertPrompt
),
"User request:"
,
"User request:"
,
userPrompt
userPrompt
].
join
(
"
\n\n
"
);
].
join
(
"
\n\n
"
);
...
@@ -71,8 +81,10 @@ function resolveDeclaredWorkspaceEntry(projectConfig?: ProjectPackageConfig | nu
...
@@ -71,8 +81,10 @@ function resolveDeclaredWorkspaceEntry(projectConfig?: ProjectPackageConfig | nu
}
}
export
class
ProjectExecutionRouter
{
export
class
ProjectExecutionRouter
{
constructor
(
private
readonly
systemSummary
:
SystemSummary
)
{}
async
decide
(
request
:
ProjectExecutionRequest
):
Promise
<
ProjectExecutionDecision
>
{
async
decide
(
request
:
ProjectExecutionRequest
):
Promise
<
ProjectExecutionDecision
>
{
const
preparedPrompt
=
buildPreparedPrompt
(
request
.
context
,
request
.
userPrompt
);
const
preparedPrompt
=
await
buildPreparedPrompt
(
this
.
systemSummary
,
request
.
context
,
request
.
userPrompt
);
const
declaredWorkspaceEntryReason
=
resolveDeclaredWorkspaceEntry
(
request
.
projectConfig
);
const
declaredWorkspaceEntryReason
=
resolveDeclaredWorkspaceEntry
(
request
.
projectConfig
);
if
(
declaredWorkspaceEntryReason
&&
(
!
request
.
selectedSkillId
||
isPublishIntentPrompt
(
request
.
userPrompt
)))
{
if
(
declaredWorkspaceEntryReason
&&
(
!
request
.
selectedSkillId
||
isPublishIntentPrompt
(
request
.
userPrompt
)))
{
return
{
return
{
...
...
apps/desktop/src/main/services/project-store.ts
View file @
82f454d3
...
@@ -234,6 +234,19 @@ async function pathExists(targetPath: string): Promise<boolean> {
...
@@ -234,6 +234,19 @@ async function pathExists(targetPath: string): Promise<boolean> {
}
}
}
}
async
function
hasDirectProjectChildren
(
rootPath
:
string
):
Promise
<
boolean
>
{
const
entries
=
await
readdir
(
rootPath
,
{
withFileTypes
:
true
}).
catch
(()
=>
[]);
for
(
const
entry
of
entries
)
{
if
(
!
entry
.
isDirectory
())
{
continue
;
}
if
(
await
pathExists
(
path
.
join
(
rootPath
,
entry
.
name
,
PROJECT_FILE
)))
{
return
true
;
}
}
return
false
;
}
async
function
readJsonFile
<
T
>
(
filePath
:
string
):
Promise
<
T
|
null
>
{
async
function
readJsonFile
<
T
>
(
filePath
:
string
):
Promise
<
T
|
null
>
{
try
{
try
{
const
raw
=
await
readFile
(
filePath
,
"utf8"
);
const
raw
=
await
readFile
(
filePath
,
"utf8"
);
...
@@ -450,7 +463,15 @@ export class ProjectStoreService {
...
@@ -450,7 +463,15 @@ export class ProjectStoreService {
async
getWorkspaceRoot
():
Promise
<
string
>
{
async
getWorkspaceRoot
():
Promise
<
string
>
{
const
config
=
await
this
.
configService
.
load
();
const
config
=
await
this
.
configService
.
load
();
const
configured
=
config
.
workspacePath
.
trim
();
const
configured
=
config
.
workspacePath
.
trim
();
const
workspaceRoot
=
configured
||
this
.
configService
.
getDataPath
(
"workspace"
);
const
fallbackRoot
=
this
.
configService
.
getDataPath
(
"workspace"
);
const
configuredRoot
=
configured
||
fallbackRoot
;
const
nestedWorkspaceRoot
=
configured
?
path
.
join
(
configuredRoot
,
"workspace"
)
:
""
;
const
workspaceRoot
=
configured
&&
!
await
pathExists
(
path
.
join
(
configuredRoot
,
PROJECTS_DIR
))
&&
!
await
hasDirectProjectChildren
(
configuredRoot
)
&&
await
hasDirectProjectChildren
(
nestedWorkspaceRoot
)
?
nestedWorkspaceRoot
:
configuredRoot
;
await
mkdir
(
workspaceRoot
,
{
recursive
:
true
});
await
mkdir
(
workspaceRoot
,
{
recursive
:
true
});
return
workspaceRoot
;
return
workspaceRoot
;
}
}
...
@@ -981,32 +1002,42 @@ export class ProjectStoreService {
...
@@ -981,32 +1002,42 @@ export class ProjectStoreService {
}
}
private
async
readProjects
(
options
?:
{
includeBuiltinHome
?:
boolean
}):
Promise
<
ProjectSummary
[]
>
{
private
async
readProjects
(
options
?:
{
includeBuiltinHome
?:
boolean
}):
Promise
<
ProjectSummary
[]
>
{
const
workspaceRoot
=
await
this
.
getWorkspaceRoot
();
const
projects
=
new
Map
<
string
,
ProjectSummary
>
();
const
projectsRoot
=
path
.
join
(
workspaceRoot
,
PROJECTS_DIR
);
for
(
const
rootPath
of
await
this
.
getProjectContainerRoots
())
{
const
entries
=
await
readdir
(
projectsRoot
,
{
withFileTypes
:
true
}).
catch
(()
=>
[]);
const
entries
=
await
readdir
(
rootPath
,
{
withFileTypes
:
true
}).
catch
(()
=>
[]);
const
projects
:
ProjectSummary
[]
=
[];
for
(
const
entry
of
entries
)
{
for
(
const
entry
of
entries
)
{
if
(
!
entry
.
isDirectory
())
{
if
(
!
entry
.
isDirectory
())
{
continue
;
continue
;
}
}
const
projectDir
=
this
.
resolveWorkspaceChildPath
(
rootPath
,
entry
.
name
);
const
record
=
await
this
.
readProjectRecord
(
entry
.
name
);
const
record
=
await
readJsonFile
<
StoredProjectRecord
>
(
path
.
join
(
projectDir
,
PROJECT_FILE
));
if
(
!
record
)
{
if
(
!
record
)
{
continue
;
continue
;
}
}
if
(
!
options
?.
includeBuiltinHome
&&
isBuiltinHomeProjectId
(
record
.
id
))
{
if
(
!
options
?.
includeBuiltinHome
&&
isBuiltinHomeProjectId
(
record
.
id
))
{
continue
;
continue
;
}
if
(
!
projects
.
has
(
record
.
id
))
{
projects
.
set
(
record
.
id
,
this
.
toProjectSummary
(
record
));
}
}
}
projects
.
push
(
this
.
toProjectSummary
(
record
));
}
}
return
projects
;
return
[...
projects
.
values
()]
;
}
}
private
async
readProjectRecord
(
projectId
:
string
):
Promise
<
StoredProjectRecord
|
null
>
{
private
async
readProjectRecord
(
projectId
:
string
):
Promise
<
StoredProjectRecord
|
null
>
{
const
existingDir
=
await
this
.
resolveExistingProjectDir
(
projectId
);
if
(
existingDir
)
{
return
readJsonFile
<
StoredProjectRecord
>
(
path
.
join
(
existingDir
,
PROJECT_FILE
));
}
return
readJsonFile
<
StoredProjectRecord
>
(
path
.
join
(
await
this
.
getProjectDir
(
projectId
),
PROJECT_FILE
));
return
readJsonFile
<
StoredProjectRecord
>
(
path
.
join
(
await
this
.
getProjectDir
(
projectId
),
PROJECT_FILE
));
}
}
private
toProjectSummary
(
record
:
StoredProjectRecord
):
ProjectSummary
{
private
toProjectSummary
(
record
:
StoredProjectRecord
):
ProjectSummary
{
const
packageConfig
=
normalizeProjectPackageConfig
(
record
);
const
packageConfig
=
normalizeProjectPackageConfig
(
record
);
const
updatedAt
=
typeof
record
.
updatedAt
===
"string"
&&
record
.
updatedAt
.
trim
()
?
record
.
updatedAt
:
nowIso
();
return
{
return
{
id
:
record
.
id
,
id
:
record
.
id
,
name
:
record
.
name
,
name
:
record
.
name
,
...
@@ -1020,7 +1051,7 @@ export class ProjectStoreService {
...
@@ -1020,7 +1051,7 @@ export class ProjectStoreService {
isBuiltinHome
:
isBuiltinHomeProjectId
(
record
.
id
),
isBuiltinHome
:
isBuiltinHomeProjectId
(
record
.
id
),
description
:
record
.
description
,
description
:
record
.
description
,
version
:
record
.
version
,
version
:
record
.
version
,
updatedAt
:
record
.
updatedAt
,
updatedAt
,
skillCount
:
record
.
boundSkillIds
?.
length
??
0
,
skillCount
:
record
.
boundSkillIds
?.
length
??
0
,
ready
:
record
.
ready
!==
false
,
ready
:
record
.
ready
!==
false
,
projectType
:
packageConfig
?.
projectType
,
projectType
:
packageConfig
?.
projectType
,
...
@@ -1035,9 +1066,32 @@ export class ProjectStoreService {
...
@@ -1035,9 +1066,32 @@ export class ProjectStoreService {
}
}
private
async
getProjectDir
(
projectId
:
string
):
Promise
<
string
>
{
private
async
getProjectDir
(
projectId
:
string
):
Promise
<
string
>
{
const
existingDir
=
await
this
.
resolveExistingProjectDir
(
projectId
);
if
(
existingDir
)
{
return
existingDir
;
}
return
this
.
resolveWorkspaceChildPath
(
path
.
join
(
await
this
.
getWorkspaceRoot
(),
PROJECTS_DIR
),
projectId
);
return
this
.
resolveWorkspaceChildPath
(
path
.
join
(
await
this
.
getWorkspaceRoot
(),
PROJECTS_DIR
),
projectId
);
}
}
private
async
getProjectContainerRoots
():
Promise
<
string
[]
>
{
const
workspaceRoot
=
await
this
.
getWorkspaceRoot
();
return
[...
new
Set
([
workspaceRoot
,
path
.
join
(
workspaceRoot
,
"workspace"
),
path
.
join
(
workspaceRoot
,
PROJECTS_DIR
)
].
map
((
rootPath
)
=>
path
.
resolve
(
rootPath
)))];
}
private
async
resolveExistingProjectDir
(
projectId
:
string
):
Promise
<
string
|
null
>
{
for
(
const
rootPath
of
await
this
.
getProjectContainerRoots
())
{
const
projectDir
=
this
.
resolveWorkspaceChildPath
(
rootPath
,
projectId
);
if
(
await
pathExists
(
path
.
join
(
projectDir
,
PROJECT_FILE
)))
{
return
projectDir
;
}
}
return
null
;
}
private
async
listWorkspaceSkills
(
private
async
listWorkspaceSkills
(
projectName
:
string
,
projectName
:
string
,
projectUpdatedAt
:
string
,
projectUpdatedAt
:
string
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment