feat(web): enable AI page data loading in web mode via HTTP endpoints

- Move assistant/skill MD parsers to @openchatlab/node-runtime (shared)
- Move BUILTIN_TOOL_CATALOG, provider-registry, model-catalog to @openchatlab/core
- Add /_web/ai/* HTTP routes for assistants, skills, LLM config, providers, models, tools
- Frontend shims: read ops call HTTP, write/agent ops return stubs
- Remove IS_ELECTRON guard in llm.ts to enable web-mode config loading
- Improve web-mode AI chat error message to be user-friendly
This commit is contained in:
digua committed 2026-05-14 20:29:06 +08:00
1 parent 4b15d65b13
commit 12f9ae90eb
23 files changed
+1879 -1372

No files matched your search

+2 -1
View File
@@ -6,6 +6,7 @@
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"@openchatlab/core": "workspace:*"
"@openchatlab/core": "workspace:*",
"gray-matter": "^4.0.3"
}
}
@@ -0,0 +1,56 @@
/**
* 助手 MD 文件解析器(平台无关,Node.js 实现)
*/
import * as path from 'path'
import matter from 'gray-matter'
import type { AssistantConfig } from './types'
export function parseAssistantFile(content: string, filePath: string): AssistantConfig | null {
try {
const { data: fm, content: body } = matter(content)
const id = fm.id ?? path.basename(filePath, '.md')
const name = fm.name
if (!name) return null
return {
id,
name,
systemPrompt: body.trim(),
presetQuestions: parseStringArray(fm.presetQuestions),
allowedBuiltinTools: parseStringArray(fm.allowedBuiltinTools),
builtinId: typeof fm.builtinId === 'string' ? fm.builtinId : undefined,
applicableChatTypes: parseChatTypes(fm.applicableChatTypes),
supportedLocales: parseStringArray(fm.supportedLocales),
}
} catch {
return null
}
}
export function serializeAssistant(config: AssistantConfig): string {
const fm: Record<string, unknown> = {
id: config.id,
name: config.name,
}
if (config.builtinId) fm.builtinId = config.builtinId
if (config.applicableChatTypes?.length) fm.applicableChatTypes = config.applicableChatTypes
if (config.supportedLocales?.length) fm.supportedLocales = config.supportedLocales
if (config.allowedBuiltinTools?.length) fm.allowedBuiltinTools = config.allowedBuiltinTools
if (config.presetQuestions?.length) fm.presetQuestions = config.presetQuestions
return matter.stringify(`\n${config.systemPrompt}\n`, fm)
}
function parseStringArray(raw: unknown): string[] {
if (Array.isArray(raw)) return raw.map(String).filter(Boolean)
return []
}
function parseChatTypes(raw: unknown): ('group' | 'private')[] | undefined {
if (!Array.isArray(raw)) return undefined
const valid = raw.filter((v): v is 'group' | 'private' => v === 'group' || v === 'private')
return valid.length > 0 ? valid : undefined
}
+9
View File
@@ -0,0 +1,9 @@
/**
* AI 模块(Node.js 实现)
*
* 助手/技能 MD 文件解析器和共享类型。
*/
export type { AssistantConfig, AssistantSummary, SkillDef, SkillSummary } from './types'
export { parseAssistantFile, serializeAssistant } from './assistant-parser'
export { parseSkillFile, extractSkillId } from './skill-parser'
@@ -0,0 +1,54 @@
/**
* 技能 MD 文件解析器(平台无关,Node.js 实现)
*/
import * as path from 'path'
import matter from 'gray-matter'
import type { SkillDef } from './types'
export function parseSkillFile(content: string, filePath: string): SkillDef | null {
try {
const { data: fm, content: prompt } = matter(content)
const id = fm.id ?? path.basename(filePath, '.md')
const name = fm.name
if (!name) return null
return {
id,
name,
description: fm.description ?? '',
tags: parseTags(fm.tags),
chatScope: validateChatScope(fm.chatScope),
tools: Array.isArray(fm.tools) ? fm.tools : [],
prompt: prompt.trim(),
}
} catch {
return null
}
}
export function extractSkillId(content: string, filePath: string): string | null {
try {
const { data: fm } = matter(content)
return fm.id ?? path.basename(filePath, '.md')
} catch {
return null
}
}
function parseTags(raw: unknown): string[] {
if (typeof raw === 'string') {
return raw
.split(',')
.map((t) => t.trim())
.filter(Boolean)
}
if (Array.isArray(raw)) return raw.map(String).filter(Boolean)
return []
}
function validateChatScope(raw: unknown): 'all' | 'group' | 'private' {
if (raw === 'group' || raw === 'private') return raw
return 'all'
}
+47
View File
@@ -0,0 +1,47 @@
/**
* AI 模块共享类型(助手、技能的解析结构)
*
* 与 Electron 端 types.ts 的同名接口结构一致(TypeScript 结构类型兼容)。
*/
export interface AssistantConfig {
id: string
name: string
systemPrompt: string
presetQuestions: string[]
allowedBuiltinTools?: string[]
builtinId?: string
applicableChatTypes?: ('group' | 'private')[]
supportedLocales?: string[]
}
export interface AssistantSummary {
id: string
name: string
systemPrompt: string
presetQuestions: string[]
builtinId?: string
applicableChatTypes?: ('group' | 'private')[]
supportedLocales?: string[]
}
export interface SkillDef {
id: string
name: string
description: string
tags: string[]
chatScope: 'all' | 'group' | 'private'
prompt: string
tools: string[]
builtinId?: string
}
export interface SkillSummary {
id: string
name: string
description: string
tags: string[]
chatScope: 'all' | 'group' | 'private'
tools: string[]
builtinId?: string
}
+4
View File
@@ -29,3 +29,7 @@ export {
deleteDict,
ensureDefaultDict,
} from './nlp'
// AI 助手/技能解析器
export type { AssistantConfig, AssistantSummary, SkillDef, SkillSummary } from './ai'
export { parseAssistantFile, serializeAssistant, parseSkillFile, extractSkillId } from './ai'