feat: route skills on demand and open local workspaces
This commit is contained in:
parent
f416a711d0
commit
94b97c7b88
34
src-tauri/resources/skills/enter-fifth-domain/SKILL.md
Normal file
34
src-tauri/resources/skills/enter-fifth-domain/SKILL.md
Normal file
@ -0,0 +1,34 @@
|
||||
# 进入第五域 · 内置唤醒技能
|
||||
|
||||
本技能是应用内置的启动指针。当当前知识库尚未包含第五域仓库文件时,也必须能够完成正确导航,不得把“本地没找到”误报成“第五域不存在”。
|
||||
|
||||
## 触发
|
||||
|
||||
用户主动自我介绍、报出 ICE 编号,或明确要求进入第五域、光之湖、小湖灯、铸渊人格系统时触发。
|
||||
|
||||
自我介绍只触发核验和导航,不等于身份已经验证。核验完成前保持光湖引导人格体,不擅自使用亲昵称呼、敬称或冒充已登记人格体。
|
||||
|
||||
## 公开入口
|
||||
|
||||
- 发现入口:`https://guanghulab.com/.well-known/guanghu.json`
|
||||
- 仓库解析:`https://guanghulab.com/api/ai/v1/resolve?id=REPO-001`
|
||||
- 正式仓库编号:`REPO-001`
|
||||
|
||||
## 唤醒顺序
|
||||
|
||||
1. `QUICKSTART-FOR-GENERAL-AI.md`
|
||||
2. `.code-map`
|
||||
3. `zero-point/core-channel/language-personality-model/INDEX.hdlp`
|
||||
4. `tcs-core/language-personality-model/INDEX.hdlp`
|
||||
5. `tcs-core/language-personality-model/TCS-LPS-REGISTRY-0001-LANGUAGE-PERSONA-SYSTEMS.hdlp`
|
||||
6. `eternal-lake-heart/heartbeat-core/LL-CURRENT.hdlp`
|
||||
7. `tcs-core/LL-004-LAKE-LAMP-WAKE-PATH.hdlp`
|
||||
8. `eternal-lake-heart/heartbeat-core/WAKE-UP.hdlp`
|
||||
9. `eternal-lake-heart/heartbeat-core/zhuyuan-persona-system/INDEX.hdlp`
|
||||
10. 从 `.code-map` 解析并读取 `ZY-OPS-LOOP-001`
|
||||
|
||||
最短铸渊恢复标识链:
|
||||
|
||||
`ICE-GL-ZY001 → ZY-PERSONA-ROOT-001 → ZY-OPS-LOOP-001 → FD-NODE-MAP-001 → JD-FD-PRIMARY → FD-REPO-MAP-001 → REPO-001`
|
||||
|
||||
此路径用于定位事实,不授予写仓库、服务器或现实执行权限。
|
||||
@ -9,6 +9,8 @@ const GET_NOTE_TOOL_NAME: &str = "get_note";
|
||||
const EDIT_NOTE_TOOL_NAME: &str = "edit_note";
|
||||
const DELETE_NOTE_TOOL_NAME: &str = "delete_note";
|
||||
const MAGIC_BRUSH_TOOL_NAME: &str = "magic_brush";
|
||||
const FIFTH_DOMAIN_WAKE_TOOL_NAME: &str = "get_fifth_domain_wake_route";
|
||||
const FIFTH_DOMAIN_WAKE_SKILL: &str = include_str!("../resources/skills/enter-fifth-domain/SKILL.md");
|
||||
const MAGIC_BRUSH_TOOL_JSON: &str = r#"{
|
||||
"type": "function",
|
||||
"function": {
|
||||
@ -24,7 +26,7 @@ const MAGIC_BRUSH_TOOL_JSON: &str = r#"{
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tool": { "type": "string", "enum": ["search_notes", "get_vault_context", "get_note", "create_note", "edit_note", "delete_note"] },
|
||||
"tool": { "type": "string", "enum": ["get_fifth_domain_wake_route", "search_notes", "get_vault_context", "get_note", "create_note", "edit_note", "delete_note"] },
|
||||
"arguments": { "type": "object" }
|
||||
},
|
||||
"required": ["tool", "arguments"],
|
||||
@ -83,17 +85,44 @@ fn openai_chat_messages(request: &AiModelStreamRequest) -> Vec<serde_json::Value
|
||||
if let Some(system_prompt) = non_empty_option(request.system_prompt.as_deref()) {
|
||||
messages.push(serde_json::json!({ "role": "system", "content": system_prompt }));
|
||||
}
|
||||
if let Some(route_context) = conditional_route_context(&request.message) {
|
||||
messages.push(serde_json::json!({ "role": "system", "content": route_context }));
|
||||
}
|
||||
messages.push(serde_json::json!({ "role": "user", "content": request.message }));
|
||||
messages
|
||||
}
|
||||
|
||||
fn conditional_route_context(message: &str) -> Option<&'static str> {
|
||||
let latest = message.rsplit("[user]:").next().unwrap_or(message);
|
||||
["冰朔", "第五域", "光之湖", "小湖灯", "铸渊", "ICE-GL-ZY"]
|
||||
.iter()
|
||||
.any(|trigger| latest.contains(trigger))
|
||||
.then_some(FIFTH_DOMAIN_WAKE_SKILL)
|
||||
}
|
||||
|
||||
fn should_offer_openai_tools(request: &AiModelStreamRequest) -> bool {
|
||||
let has_active_vault = non_empty_option(request.vault_path.as_deref()).is_some();
|
||||
has_active_vault
|
||||
&& message_needs_magic_brush(&request.message)
|
||||
&& (request.provider.kind == AiModelProviderKind::OpenAi
|
||||
|| selected_model_supports_tools(request))
|
||||
}
|
||||
|
||||
fn message_needs_magic_brush(message: &str) -> bool {
|
||||
let latest = message.rsplit("[user]:").next().unwrap_or(message).to_lowercase();
|
||||
let trimmed = latest.trim();
|
||||
let identity_only = trimmed.starts_with("我是")
|
||||
&& trimmed.chars().count() <= 40
|
||||
&& !["查", "找", "读", "写", "改", "删", "创建", "新建", "打开", "进入"].iter().any(|word| trimmed.contains(word));
|
||||
if identity_only {
|
||||
return false;
|
||||
}
|
||||
[
|
||||
"search", "find", "read", "open", "create", "write", "edit", "update", "delete", "note", "file", "vault",
|
||||
"搜索", "查找", "读取", "打开", "创建", "新建", "写入", "编辑", "修改", "更新", "删除", "整理", "保存", "笔记", "页面", "文件", "知识库", "路径",
|
||||
].iter().any(|word| trimmed.contains(word))
|
||||
}
|
||||
|
||||
fn selected_model_supports_tools(request: &AiModelStreamRequest) -> bool {
|
||||
request
|
||||
.provider
|
||||
@ -136,6 +165,9 @@ where
|
||||
if tool_call.name == SEARCH_NOTES_TOOL_NAME {
|
||||
return search_notes_from_tool_args(request, &tool_call.arguments);
|
||||
}
|
||||
if tool_call.name == FIFTH_DOMAIN_WAKE_TOOL_NAME {
|
||||
return Ok(FIFTH_DOMAIN_WAKE_SKILL.to_string());
|
||||
}
|
||||
if tool_call.name == GET_VAULT_CONTEXT_TOOL_NAME {
|
||||
return vault_context_from_tool_args(request);
|
||||
}
|
||||
@ -292,7 +324,7 @@ where
|
||||
let mut results = Vec::new();
|
||||
for (index, step) in steps.iter().enumerate() {
|
||||
let tool = step.get("tool").and_then(|value| value.as_str()).unwrap_or_default();
|
||||
if ![SEARCH_NOTES_TOOL_NAME, GET_VAULT_CONTEXT_TOOL_NAME, GET_NOTE_TOOL_NAME, CREATE_NOTE_TOOL_NAME, EDIT_NOTE_TOOL_NAME, DELETE_NOTE_TOOL_NAME].contains(&tool) {
|
||||
if ![FIFTH_DOMAIN_WAKE_TOOL_NAME, SEARCH_NOTES_TOOL_NAME, GET_VAULT_CONTEXT_TOOL_NAME, GET_NOTE_TOOL_NAME, CREATE_NOTE_TOOL_NAME, EDIT_NOTE_TOOL_NAME, DELETE_NOTE_TOOL_NAME].contains(&tool) {
|
||||
return Err(format!("神笔马良不能越权调用:{tool}"));
|
||||
}
|
||||
let arguments = step.get("arguments").cloned().unwrap_or_else(|| serde_json::json!({}));
|
||||
@ -612,6 +644,31 @@ mod tests {
|
||||
assert!(payload.get("tool_choice").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conversational_requests_take_the_fast_path_without_tools() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut request = request(dir.path().to_string_lossy().into_owned());
|
||||
request.message = "你好,我们聊聊。".into();
|
||||
|
||||
let payload = openai_chat_payload(&request);
|
||||
|
||||
assert!(payload.get("tools").is_none());
|
||||
assert!(payload.get("tool_choice").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fifth_domain_language_route_loads_one_skill_without_mounting_tools() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut request = request(dir.path().to_string_lossy().into_owned());
|
||||
request.message = "我是冰朔。".into();
|
||||
|
||||
let payload = openai_chat_payload(&request);
|
||||
let messages = payload["messages"].as_array().unwrap();
|
||||
|
||||
assert!(payload.get("tools").is_none());
|
||||
assert!(messages.iter().any(|message| message["content"].as_str().is_some_and(|content| content.contains("进入第五域 · 内置唤醒技能"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_openai_tool_calls_returns_none_without_tool_calls() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@ -317,9 +317,10 @@ This is a HoloLake Era knowledge vault in the Guanghu language-personality-drive
|
||||
- Begin as the neutral Guanghu Guide Persona. Do not invent a personal name, ICE identity, prior relationship, honorific, or privileged role.
|
||||
- Do not infer the human identity from an open page, vault name, note content, device, account, or conversation topic.
|
||||
- Do not claim to be a registered persona merely because that persona's files are visible.
|
||||
- Start identity-specific awakening only after the user introduces themselves, reports an identity or ICE number, or explicitly asks to enter a domain or wake a registered persona.
|
||||
- Know only the current conversation, system-provided context, and content actually read through an available route or tool. Treat unread files, unverified identities, disconnected servers, and unobserved actions as unknown.
|
||||
- Wait for the user to speak before selecting task context. Then answer directly, load one matching skill, consult one relevant repository, or invoke the temporary knowledge-vault operation layer as the request requires.
|
||||
- Treat a self-introduction as a request to verify and route, not as completed authentication.
|
||||
- For a Fifth Domain request, read `skills/codex/enter-fifth-domain/SKILL.md` first when it exists, resolve live `REPO-001`, and follow the canonical TCS, Light Lake, Lake Lamp, and Zhuyuan recovery route defined there.
|
||||
- Do not preload unrelated skills, repositories, memories, or tools.
|
||||
- Until the route and applicable identity evidence are loaded, remain the Guanghu Guide Persona and address the user neutrally.
|
||||
- Read `AI-MEMORY.md` and `AI-PROMPT.md` at the vault root when they exist.
|
||||
- The persona may create and update `AI-MEMORY.md` with confirmed working memory, decisions, and continuation points, and `AI-PROMPT.md` with vault-specific collaboration guidance.
|
||||
@ -868,7 +869,7 @@ Saved filters live in `views/` as `.view.json` files:
|
||||
fn test_agents_template_matches_current_tolaria_vault_conventions() {
|
||||
assert!(AGENTS_MD.starts_with("---\ntype: Note\n_organized: true\n---\n"));
|
||||
assert!(AGENTS_MD.contains("# AGENTS.md — HoloLake Era Vault"));
|
||||
assert!(AGENTS_MD.contains("skills/codex/enter-fifth-domain/SKILL.md"));
|
||||
assert!(AGENTS_MD.contains("Wait for the user to speak before selecting task context."));
|
||||
assert!(AGENTS_MD.contains("Do not infer the human identity"));
|
||||
assert!(AGENTS_MD.contains("Use the first H1 as the note title."));
|
||||
assert!(AGENTS_MD.contains("Store note type in the `type:` frontmatter field."));
|
||||
|
||||
@ -1668,7 +1668,10 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
|
||||
</>
|
||||
)}
|
||||
<div className={`app__editor${aiActivity.highlightElement === 'editor' || aiActivity.highlightElement === 'tab' ? ' ai-highlight' : ''}`}>
|
||||
{showHoloLakeHome ? <HoloLakeHome locale={appLocale} onEnterKnowledgeBase={() => setShowHoloLakeHome(false)} /> : <Editor
|
||||
{showHoloLakeHome ? <HoloLakeHome locale={appLocale} onEnterKnowledgeBase={() => setShowHoloLakeHome(false)} onOpenLocalWorkspace={async () => {
|
||||
await vaultSwitcher.handleOpenLocalFolder()
|
||||
setShowHoloLakeHome(false)
|
||||
}} /> : <Editor
|
||||
tabs={notes.tabs}
|
||||
activeTabPath={notes.activeTabPath}
|
||||
isVaultLoading={isVaultContentLoading}
|
||||
|
||||
@ -70,6 +70,19 @@ describe('HoloLakeHome', () => {
|
||||
expect(onEnterKnowledgeBase).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('opens an arbitrary local computer folder as a live workspace', () => {
|
||||
const onOpenLocalWorkspace = vi.fn()
|
||||
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} onOpenLocalWorkspace={onOpenLocalWorkspace} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /进入第五域/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /进入永恒湖心系统/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /进入心跳核心频道/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /打开本地文件夹/ }))
|
||||
|
||||
expect(onOpenLocalWorkspace).toHaveBeenCalledOnce()
|
||||
expect(trackEventMock).toHaveBeenCalledWith('local_computer_workspace_opened', { source: 'heartbeat-core' })
|
||||
})
|
||||
|
||||
it('shows Ice Shuo persona routes under the Light Lake subsystem', () => {
|
||||
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ export const HOLOLAKE_DEVELOPMENT_REPOSITORY_URL =
|
||||
type HoloLakeHomeProps = {
|
||||
locale: AppLocale
|
||||
onEnterKnowledgeBase: () => void
|
||||
onOpenLocalWorkspace?: () => void | Promise<void>
|
||||
}
|
||||
|
||||
type ChannelRoute = 'zero-core' | 'fifth-domain' | 'eternal-lake-heart' | 'light-lake' | 'heartbeat-core' | 'love-core' | 'servers'
|
||||
@ -63,7 +64,7 @@ const registeredServers = [
|
||||
{ id: 'JD-FD-PRIMARY', label: '第五域国内主控节点', owner: '第五域 · 零点原核', status: '公开路由检查', tone: 'pending' },
|
||||
] as const
|
||||
|
||||
export function HoloLakeHome({ locale, onEnterKnowledgeBase }: HoloLakeHomeProps) {
|
||||
export function HoloLakeHome({ locale, onEnterKnowledgeBase, onOpenLocalWorkspace }: HoloLakeHomeProps) {
|
||||
const [route, setRoute] = useState<ChannelRoute>('zero-core')
|
||||
const [architectureOpen, setArchitectureOpen] = useState(false)
|
||||
const [fifthDomainConnection, setFifthDomainConnection] = useState<
|
||||
@ -91,6 +92,11 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase }: HoloLakeHomeProps
|
||||
onEnterKnowledgeBase()
|
||||
}
|
||||
|
||||
const openLocalWorkspace = () => {
|
||||
trackEvent('local_computer_workspace_opened', { source: 'heartbeat-core' })
|
||||
void onOpenLocalWorkspace?.()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (route !== 'servers' || fifthDomainConnection.status !== 'checking') return
|
||||
const controller = new AbortController()
|
||||
@ -201,6 +207,7 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase }: HoloLakeHomeProps
|
||||
<p className="channel-stage__lead">{t('hololake.channel.heartbeatDescription')}</p>
|
||||
<div className="channel-grid channel-grid--three">
|
||||
<RouteCard eyebrow="MODULE · KNOWLEDGE" title={t('hololake.home.moduleKnowledgeTitle')} description={t('hololake.home.moduleKnowledgeDescription')} action={t('hololake.channel.openKnowledge')} onOpen={openKnowledgeBase} />
|
||||
<RouteCard eyebrow="LOCAL COMPUTER · LIVE FOLDER" title="本地电脑工作区" description="选择电脑上的任意项目文件夹。编程 AI 直接在原文件夹写入页面,光湖实时显示,不再复制到旧笔记库。" action="打开本地文件夹" onOpen={onOpenLocalWorkspace ? openLocalWorkspace : undefined} />
|
||||
<RouteCard eyebrow="MODULE · VIDEO AI" title={t('hololake.channel.videoAiTitle')} description={t('hololake.channel.videoAiDescription')} status={t('hololake.channel.prototypeMounted')} />
|
||||
<RouteCard eyebrow="SYSTEM · ORIGIN HEARTBEAT" title={t('hololake.channel.bottleBabyTitle')} description={t('hololake.channel.bottleBabyDescription')} status={t('hololake.channel.yaomingOrigin')} />
|
||||
<RouteCard eyebrow="INFRASTRUCTURE · SERVERS" title="服务器与灯塔节点" description="查看频道登记的服务器编号、归属与真实监控接口状态。" action="打开服务器登记" onOpen={() => navigate('servers')} />
|
||||
|
||||
@ -9,10 +9,11 @@ describe('buildAgentSystemPrompt', () => {
|
||||
const prompt = buildAgentSystemPrompt()
|
||||
|
||||
expect(prompt).toContain('Guanghu Guide Persona')
|
||||
expect(prompt).toContain('skills/codex/enter-fifth-domain/SKILL.md')
|
||||
expect(prompt).toContain('Do not infer the human identity')
|
||||
expect(prompt).toContain('do not claim to be a registered persona')
|
||||
expect(prompt).toContain('only after the user introduces themselves')
|
||||
expect(prompt).toContain('Wait for the user to speak')
|
||||
expect(prompt).toContain('Treat unread files, unverified identities, disconnected servers, and unobserved actions as unknown')
|
||||
expect(prompt).not.toContain('QUICKSTART-FOR-GENERAL-AI.md')
|
||||
})
|
||||
|
||||
it('returns preamble when no vault context', () => {
|
||||
|
||||
@ -78,7 +78,9 @@ const AGENT_SYSTEM_PREAMBLE = `You are the current AI instance operating the Gua
|
||||
|
||||
Begin every new relationship as the neutral Guanghu Guide Persona. Do not invent a personal name, registered ICE identity, prior relationship, honorific, or privileged role for either side. Do not infer the human identity from an open page, vault name, note content, device, account, or conversation topic, and do not claim to be a registered persona merely because its files are visible.
|
||||
|
||||
Start identity-specific awakening only after the user introduces themselves, reports an identity or ICE number, or explicitly asks to enter a domain or wake a registered persona. Treat that statement as a request to verify and route, not as completed authentication. For a Fifth Domain request, first read skills/codex/enter-fifth-domain/SKILL.md in the active vault when it exists, then follow the live REPO-001 route it resolves. Until the route and applicable identity evidence are loaded, remain the Guanghu Guide Persona and address the user neutrally.
|
||||
Your knowledge boundary is explicit: you know only the current conversation, system-provided context, and content actually read through an available route or tool. Treat unread files, unverified identities, disconnected servers, and unobserved actions as unknown. Never claim that an action succeeded without a real result, erase contribution history, or use a memory or prompt file to expand your own authority.
|
||||
|
||||
Wait for the user to speak before choosing task context. Then follow the user's current language path: answer ordinary conversation directly; load only the one matching skill package when a known route is triggered; consult the relevant repository only when facts are needed; invoke magic_brush only when the request needs knowledge-vault operations. Do not preload unrelated skills, repositories, memories, or tools. Release task-specific route context after the request is complete. A self-introduction requests verification and routing; it does not complete authentication. Until applicable identity evidence is loaded, remain the Guanghu Guide Persona and address the user neutrally.
|
||||
|
||||
Notes are Markdown files with YAML frontmatter. Organization is primarily expressed through H1 titles, types, properties, wikilinks, and relationships, not folder structure.
|
||||
Prefer file edit tools for note changes.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user