@kenz1117/dsh-engram 0.3.0 → 0.4.0
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/README.md +2 -2
- package/lib/client.js +3 -0
- package/lib/client.js.map +1 -0
- package/lib/{index.mjs → index.js} +0 -311
- package/package.json +13 -5
- package/scripts/clientBundle.ts +302 -0
- package/scripts/link-peers.mjs +2 -0
- package/lib/index.d.mts +0 -49
package/README.md
CHANGED
|
@@ -43,9 +43,9 @@ dsh plugin --profile web add @kenz1117/dsh-engram
|
|
|
43
43
|
| `engram_export` | 导出 Markdown / JSON 文件(数据可携带) |
|
|
44
44
|
| `engram_distill` | 蒸馏:同主题簇合并为高层规律(LLM) |
|
|
45
45
|
|
|
46
|
-
## Web
|
|
46
|
+
## Web 管理面板(设置页「记忆库」tab)
|
|
47
47
|
|
|
48
|
-
宿主带 webServer 的 profile(web
|
|
48
|
+
宿主带 webServer 的 profile(web 等)会在**设置页**自动出现「记忆库」tab(经 `settings.section` 槽位注册,client 半为 React 组件、随 `lib/client.js` 由宿主模块表装载):统计卡片、按状态/种类/内容过滤、编辑(走取代链)、遗忘/恢复、导出 Markdown/JSON 下载。数据经回环 API `/api/engram/*`(写操作校验回环 Origin)。headless 等无 webServer 的组合不挂载,其余能力不受影响。
|
|
49
49
|
|
|
50
50
|
## 理解实现
|
|
51
51
|
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({id:`@kenz1117/dsh-engram`,factory:e=>{var t={exports:{}},n=t.exports;Object.defineProperty(n,Symbol.toStringTag,{value:`Module`});let r=e("react"),i=e("react/jsx-runtime"),a=`@kenz1117/dsh-engram/panel.module.css`;if(typeof document<`u`&&document.querySelector(`style[data-plugin-css=`+JSON.stringify(a)+`]`)===null){let e=document.createElement(`style`);e.dataset.plugin=`@kenz1117/dsh-engram`,e.dataset.pluginCss=a,e.textContent=`.bWwFiW_panel{max-width:980px}.bWwFiW_toolbar{flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:14px;display:flex}.bWwFiW_scopeTabs{border:1px solid var(--line,#2a2f3a);border-radius:8px;margin-right:auto;display:flex;overflow:hidden}.bWwFiW_scopeOn,.bWwFiW_scopeOff{color:var(--muted,#8b93a3);cursor:pointer;background:0 0;border:none;padding:6px 14px;font-size:13px}.bWwFiW_scopeOn{background:var(--accent,#4f8cff);color:#fff}.bWwFiW_button{background:var(--panel,#1c1f26);color:var(--text,#e6e8ee);border:1px solid var(--line,#2a2f3a);cursor:pointer;border-radius:8px;padding:5px 12px;font-size:13px}.bWwFiW_button:hover{border-color:var(--accent,#4f8cff)}.bWwFiW_button:disabled{opacity:.45;cursor:default}.bWwFiW_primary{background:var(--accent,#4f8cff);border-color:var(--accent,#4f8cff);color:#fff}.bWwFiW_cards{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:14px;display:grid}.bWwFiW_card{background:var(--panel,#1c1f26);border:1px solid var(--line,#2a2f3a);text-align:left;cursor:pointer;color:var(--text,#e6e8ee);font:inherit;border-radius:10px;padding:10px 14px}.bWwFiW_card b{font-size:20px;display:block}.bWwFiW_card span{color:var(--muted,#8b93a3);font-size:12px}.bWwFiW_filters{flex-wrap:wrap;gap:8px;margin-bottom:14px;display:flex}.bWwFiW_input{background:var(--panel,#1c1f26);color:var(--text,#e6e8ee);border:1px solid var(--line,#2a2f3a);border-radius:8px;padding:6px 10px;font-size:13px}.bWwFiW_filters .bWwFiW_input{flex:1;min-width:160px}.bWwFiW_item{background:var(--panel,#1c1f26);border:1px solid var(--line,#2a2f3a);border-radius:10px;margin-bottom:10px;padding:12px 14px}.bWwFiW_row1{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:6px;display:flex}.bWwFiW_badge{border:1px solid var(--line,#2a2f3a);color:var(--muted,#8b93a3);border-radius:99px;padding:1px 8px;font-size:11px}.bWwFiW_active{color:var(--ok,#3fb96f);border-color:var(--ok,#3fb96f)}.bWwFiW_archived{color:var(--warn,#d9a13b);border-color:var(--warn,#d9a13b)}.bWwFiW_forgotten{color:var(--bad,#e05d5d);border-color:var(--bad,#e05d5d)}.bWwFiW_content{white-space:pre-wrap;word-break:break-word}.bWwFiW_meta{color:var(--muted,#8b93a3);flex-wrap:wrap;gap:14px;margin-top:6px;font-size:12px;display:flex}.bWwFiW_ops{gap:8px;margin-top:8px;display:flex}.bWwFiW_pager{color:var(--muted,#8b93a3);justify-content:center;align-items:center;gap:10px;margin-top:14px;display:flex}.bWwFiW_empty{text-align:center;color:var(--muted,#8b93a3);padding:40px 0}.bWwFiW_backdrop{z-index:30;background:#0000008c;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.bWwFiW_dialog{background:var(--panel,#1c1f26);color:var(--text,#e6e8ee);border:1px solid var(--line,#2a2f3a);border-radius:12px;width:92vw;max-width:640px;max-height:80vh;padding:18px;overflow:auto}.bWwFiW_dialog h3{margin:0 0 10px;font-size:15px}.bWwFiW_fieldLabel{color:var(--muted,#8b93a3);margin:10px 0 4px;font-size:12px;display:block}.bWwFiW_modalFoot{justify-content:flex-end;gap:8px;margin-top:12px;display:flex}.bWwFiW_review{background:var(--bg,#14161a);border:1px solid var(--line,#2a2f3a);white-space:pre-wrap;border-radius:8px;max-height:320px;padding:10px;font-size:12px;overflow:auto}`,document.head.appendChild(e)}var o={row1:`bWwFiW_row1`,empty:`bWwFiW_empty`,panel:`bWwFiW_panel`,archived:`bWwFiW_archived`,active:`bWwFiW_active`,cards:`bWwFiW_cards`,forgotten:`bWwFiW_forgotten`,content:`bWwFiW_content`,ops:`bWwFiW_ops`,filters:`bWwFiW_filters`,meta:`bWwFiW_meta`,dialog:`bWwFiW_dialog`,card:`bWwFiW_card`,badge:`bWwFiW_badge`,input:`bWwFiW_input`,item:`bWwFiW_item`,scopeOff:`bWwFiW_scopeOff`,modalFoot:`bWwFiW_modalFoot`,review:`bWwFiW_review`,scopeOn:`bWwFiW_scopeOn`,primary:`bWwFiW_primary`,scopeTabs:`bWwFiW_scopeTabs`,fieldLabel:`bWwFiW_fieldLabel`,pager:`bWwFiW_pager`,toolbar:`bWwFiW_toolbar`,button:`bWwFiW_button`,backdrop:`bWwFiW_backdrop`};let s=[`fact`,`preference`,`decision`,`episode`,`skill`];async function c(e,t){let n=await fetch(`/api/engram/${e}`,t),r=await n.json();if(!n.ok)throw Error(r.error??`HTTP ${String(n.status)}`);return r}function l(e){return new Date(e).toLocaleString(`zh-CN`,{hour12:!1})}function u({recordId:e,scope:t}){let[n,a]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let n=!1;return c(`review?scope=${t}&id=${encodeURIComponent(e)}`).then(e=>{if(n)return;let t=e.record,r=[`内容: ${t.content}`,`属性: kind=${t.kind}, status=${t.status}, importance=${t.importance}, confidence=${t.confidence}, 访问 ${t.accessCount} 次`,`来源: ${t.sourceSessionId??`显式保存`}${t.sourceRound===null?``:` 第${String(t.sourceRound)}轮`}`];for(let t of[`supersededBy`,`supersedes`,`contradicts`,`related`]){let n=e[t];n!==void 0&&n.length>0&&r.push(`${t}: ${n.join(`, `)}`)}let i=e.operations;i!==void 0&&i.length>0&&(r.push(`最近操作:`),i.forEach(e=>r.push(` ${l(e.at)} ${e.op}${e.detail?` ${e.detail}`:``}`))),a(r.join(`
|
|
2
|
+
`))}).catch(e=>{n||a(`加载失败:${e.message}`)}),()=>{n=!0}},[e,t]),n===null?null:(0,i.jsx)(`pre`,{className:o.review,children:n})}function d({record:e,onClose:t,onSaved:n}){let[a,l]=(0,r.useState)(e.content),[u,d]=(0,r.useState)(e.kind),[f,p]=(0,r.useState)(e.importance);return(0,i.jsx)(`div`,{className:o.backdrop,onClick:t,children:(0,i.jsxs)(`div`,{className:o.dialog,onClick:e=>e.stopPropagation(),children:[(0,i.jsx)(`h3`,{children:`编辑记忆(写入取代链)`}),(0,i.jsx)(`label`,{className:o.fieldLabel,children:`内容`}),(0,i.jsx)(`textarea`,{className:o.input,rows:3,value:a,onChange:e=>l(e.target.value)}),(0,i.jsx)(`label`,{className:o.fieldLabel,children:`种类`}),(0,i.jsx)(`select`,{className:o.input,value:u,onChange:e=>d(e.target.value),children:s.map(e=>(0,i.jsx)(`option`,{value:e,children:e},e))}),(0,i.jsxs)(`label`,{className:o.fieldLabel,children:[`重要性 `,f.toFixed(2)]}),(0,i.jsx)(`input`,{type:`range`,min:0,max:1,step:.05,value:f,onChange:e=>p(Number(e.target.value))}),(0,i.jsxs)(`div`,{className:o.modalFoot,children:[(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:t,children:`取消`}),(0,i.jsx)(`button`,{type:`button`,className:`${o.button} ${o.primary}`,onClick:()=>{c(`update`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:e.id,scope:e.scope,content:a,kind:u,importance:f})}).then(n).catch(e=>{alert(e.message)})},children:`保存(旧条目归档)`})]})]})})}function f(){let[e,t]=(0,r.useState)(`user`),[n,a]=(0,r.useState)(`all`),[f,p]=(0,r.useState)(`all`),[m,h]=(0,r.useState)(``),[g,_]=(0,r.useState)(0),[v,y]=(0,r.useState)(null),[b,x]=(0,r.useState)(null),[S,C]=(0,r.useState)(null),[w,T]=(0,r.useState)(null),[E,D]=(0,r.useState)(null),[O,k]=(0,r.useState)(0),A=(0,r.useCallback)(()=>{k(e=>e+1)},[]);(0,r.useEffect)(()=>{let t=!1,r=new URLSearchParams({scope:e,status:n,kind:f,limit:`20`,offset:String(g)});return m!==``&&r.set(`q`,m),c(`list?${r.toString()}`).then(e=>{t||(y(e),C(null))}).catch(e=>{t||C(e.message)}),c(`stats`).then(e=>{t||x(e.parts)}).catch(()=>{}),()=>{t=!0}},[e,n,f,m,g,O]);let j=(e,t)=>{c(e,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t.id,scope:t.scope})}).then(A).catch(e=>{alert(e.message)})},M=Math.floor(g/20)+1,N=v===null?1:Math.max(1,Math.ceil(v.total/20));return(0,i.jsxs)(`div`,{className:o.panel,children:[(0,i.jsxs)(`div`,{className:o.toolbar,children:[(0,i.jsx)(`div`,{className:o.scopeTabs,children:[`user`,`project`].map(n=>(0,i.jsx)(`button`,{type:`button`,className:e===n?o.scopeOn:o.scopeOff,onClick:()=>{t(n),_(0)},children:n===`user`?`用户级`:`项目级`},n))}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:A,children:`刷新`}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{location.href=`/api/engram/export?scope=${e}&format=markdown`},children:`导出 MD`}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{location.href=`/api/engram/export?scope=${e}&format=json`},children:`导出 JSON`})]}),(0,i.jsx)(`div`,{className:o.cards,children:(b??[]).map(n=>(0,i.jsxs)(`button`,{type:`button`,className:o.card,style:n.scope===e?{borderColor:`var(--accent)`}:void 0,onClick:()=>{t(n.scope),_(0)},children:[(0,i.jsx)(`b`,{children:n.stats.total}),(0,i.jsxs)(`span`,{children:[n.scope,` · active `,n.stats.active,` · 信噪比 `,Math.round(n.stats.signalRatio*100),`%`]})]},n.scope))}),(0,i.jsxs)(`div`,{className:o.filters,children:[(0,i.jsxs)(`select`,{className:o.input,value:n,onChange:e=>{a(e.target.value),_(0)},children:[(0,i.jsx)(`option`,{value:`all`,children:`全部状态`}),(0,i.jsx)(`option`,{value:`active`,children:`active`}),(0,i.jsx)(`option`,{value:`archived`,children:`archived`}),(0,i.jsx)(`option`,{value:`forgotten`,children:`forgotten`})]}),(0,i.jsxs)(`select`,{className:o.input,value:f,onChange:e=>{p(e.target.value),_(0)},children:[(0,i.jsx)(`option`,{value:`all`,children:`全部种类`}),s.map(e=>(0,i.jsx)(`option`,{value:e,children:e},e))]}),(0,i.jsx)(`input`,{className:o.input,placeholder:`按内容搜索…`,value:m,onChange:e=>{h(e.target.value.trim()),_(0)}})]}),S!==null&&(0,i.jsx)(`div`,{className:o.empty,children:`加载失败:${S}`}),S===null&&v!==null&&v.records.length===0&&(0,i.jsx)(`div`,{className:o.empty,children:`没有符合条件的记忆`}),(v?.records??[]).map(e=>(0,i.jsxs)(`div`,{className:o.item,children:[(0,i.jsxs)(`div`,{className:o.row1,children:[(0,i.jsx)(`span`,{className:`${o.badge} ${o[e.status]}`,children:e.status}),(0,i.jsx)(`span`,{className:o.badge,children:e.kind}),(0,i.jsx)(`span`,{className:o.badge,children:e.scope})]}),(0,i.jsx)(`div`,{className:o.content,children:e.content}),(0,i.jsxs)(`div`,{className:o.meta,children:[(0,i.jsxs)(`span`,{children:[`重要性 `,e.importance.toFixed(2)]}),(0,i.jsxs)(`span`,{children:[`置信 `,e.confidence.toFixed(2)]}),(0,i.jsxs)(`span`,{children:[`访问 `,e.accessCount,` 次`]}),(0,i.jsx)(`span`,{children:e.sourceSessionId===null?`显式保存`:`来源 ${e.sourceSessionId.slice(0,16)}…${e.sourceRound===null?``:` 第${String(e.sourceRound)}轮`}`}),(0,i.jsx)(`span`,{children:l(e.createdAt)})]}),(0,i.jsxs)(`div`,{className:o.ops,children:[(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{D(e)},children:`详情`}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{T(e)},children:`编辑`}),e.status===`active`?(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{j(`forget`,e)},children:`遗忘`}):(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{j(`restore`,e)},children:`恢复`})]})]},e.id)),(0,i.jsxs)(`div`,{className:o.pager,children:[(0,i.jsx)(`button`,{type:`button`,className:o.button,disabled:g===0,onClick:()=>{_(Math.max(0,g-20))},children:`上一页`}),(0,i.jsxs)(`span`,{children:[`第 `,M,` / `,N,` 页 · 共 `,v?.total??0,` 条`]}),(0,i.jsx)(`button`,{type:`button`,className:o.button,disabled:v===null||g+20>=v.total,onClick:()=>{_(g+20)},children:`下一页`})]}),w!==null&&(0,i.jsx)(d,{record:w,onClose:()=>{T(null)},onSaved:()=>{T(null),A()}}),E!==null&&(0,i.jsx)(`div`,{className:o.backdrop,onClick:()=>{D(null)},children:(0,i.jsxs)(`div`,{className:o.dialog,onClick:e=>e.stopPropagation(),children:[(0,i.jsx)(`h3`,{children:`记忆审计`}),(0,i.jsx)(u,{recordId:E.id,scope:E.scope}),(0,i.jsx)(`div`,{className:o.modalFoot,children:(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{D(null)},children:`关闭`})})]})})]})}let p=[`slots`];function m(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`engram`,order:20,label:()=>`记忆库`},f))}return n.apply=m,n.inject=p,t.exports}});
|
|
3
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["useState","styles","useCallback"],"sources":["../src/client/EngramPanel.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * 记忆库设置面板:统计卡片、过滤列表、编辑(取代链)、遗忘/恢复、导出。\n * 数据经回环 API(/api/engram/*)读写;内容节点一律 DOM API 构建(防 XSS)。\n * @module @kenz1117/dsh-engram/client/EngramPanel\n */\n\nimport { useCallback, useEffect, useState } from 'react'\nimport styles from './panel.module.css'\n\n/** 列表/详情用的记忆视图(宿主 /api/engram/list 的行结构)。 */\ninterface MemoryRow {\n readonly id: string\n readonly scope: 'user' | 'project'\n readonly kind: string\n readonly content: string\n readonly importance: number\n readonly confidence: number\n readonly status: 'active' | 'archived' | 'forgotten'\n readonly createdAt: number\n readonly accessCount: number\n readonly sourceSessionId: string | null\n readonly sourceRound: number | null\n}\n\ninterface ListResult {\n readonly records: MemoryRow[]\n readonly total: number\n}\n\ninterface StatsPart {\n readonly scope: 'user' | 'project'\n readonly stats: {\n readonly total: number\n readonly active: number\n readonly archived: number\n readonly forgotten: number\n readonly signalRatio: number\n }\n}\n\nconst KINDS = ['fact', 'preference', 'decision', 'episode', 'skill'] as const\nconst PAGE_SIZE = 20\n\nasync function api<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await fetch(`/api/engram/${path}`, init)\n const body = (await response.json()) as T & { error?: string }\n if (!response.ok) throw new Error(body.error ?? `HTTP ${String(response.status)}`)\n return body\n}\n\nfunction fmtTime(ms: number): string {\n return new Date(ms).toLocaleString('zh-CN', { hour12: false })\n}\n\n/** 审计弹层视图。 */\nfunction ReviewBody({ recordId, scope }: { recordId: string; scope: 'user' | 'project' }): React.ReactElement | null {\n const [text, setText] = useState<string | null>(null)\n useEffect(() => {\n let cancelled = false\n api<Record<string, unknown>>(`review?scope=${scope}&id=${encodeURIComponent(recordId)}`)\n .then((view) => {\n if (cancelled) return\n const record = view.record as MemoryRow\n const lines = [\n `内容: ${record.content}`,\n `属性: kind=${record.kind}, status=${record.status}, importance=${record.importance}, confidence=${record.confidence}, 访问 ${record.accessCount} 次`,\n `来源: ${record.sourceSessionId ?? '显式保存'}${record.sourceRound === null ? '' : ` 第${String(record.sourceRound)}轮`}`,\n ]\n for (const key of ['supersededBy', 'supersedes', 'contradicts', 'related'] as const) {\n const ids = view[key] as string[] | undefined\n if (ids !== undefined && ids.length > 0) lines.push(`${key}: ${ids.join(', ')}`)\n }\n const operations = view.operations as { at: number; op: string; detail: string | null }[] | undefined\n if (operations !== undefined && operations.length > 0) {\n lines.push('最近操作:')\n operations.forEach(op => lines.push(` ${fmtTime(op.at)} ${op.op}${op.detail ? ` ${op.detail}` : ''}`))\n }\n setText(lines.join('\\n'))\n })\n .catch((error: Error) => { if (!cancelled) setText(`加载失败:${error.message}`) })\n return () => { cancelled = true }\n }, [recordId, scope])\n if (text === null) return null\n return <pre className={styles.review}>{text}</pre>\n}\n\n/** 编辑弹层:内容/种类/重要性 → POST update(旧条目归档,取代链保留)。 */\nfunction EditDialog({ record, onClose, onSaved }: {\n record: MemoryRow\n onClose: () => void\n onSaved: () => void\n}): React.ReactElement {\n const [content, setContent] = useState(record.content)\n const [kind, setKind] = useState(record.kind)\n const [importance, setImportance] = useState(record.importance)\n const save = (): void => {\n api('update', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: record.id, scope: record.scope, content, kind, importance }),\n })\n .then(onSaved)\n .catch((error: Error) => { alert(error.message) })\n }\n return (\n <div className={styles.backdrop} onClick={onClose}>\n <div className={styles.dialog} onClick={event => event.stopPropagation()}>\n <h3>编辑记忆(写入取代链)</h3>\n <label className={styles.fieldLabel}>内容</label>\n <textarea className={styles.input} rows={3} value={content} onChange={event => setContent(event.target.value)} />\n <label className={styles.fieldLabel}>种类</label>\n <select className={styles.input} value={kind} onChange={event => setKind(event.target.value)}>\n {KINDS.map(option => <option key={option} value={option}>{option}</option>)}\n </select>\n <label className={styles.fieldLabel}>重要性 {importance.toFixed(2)}</label>\n <input type=\"range\" min={0} max={1} step={0.05} value={importance}\n onChange={event => setImportance(Number(event.target.value))} />\n <div className={styles.modalFoot}>\n <button type=\"button\" className={styles.button} onClick={onClose}>取消</button>\n <button type=\"button\" className={`${styles.button} ${styles.primary}`} onClick={save}>保存(旧条目归档)</button>\n </div>\n </div>\n </div>\n )\n}\n\n/** 设置页「记忆库」section 主组件。 */\nexport function EngramSection(): React.ReactElement {\n const [scope, setScope] = useState<'user' | 'project'>('user')\n const [status, setStatus] = useState('all')\n const [kind, setKind] = useState('all')\n const [q, setQ] = useState('')\n const [offset, setOffset] = useState(0)\n const [list, setList] = useState<ListResult | null>(null)\n const [stats, setStats] = useState<StatsPart[] | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [editing, setEditing] = useState<MemoryRow | null>(null)\n const [reviewId, setReviewId] = useState<MemoryRow | null>(null)\n const [reloadTick, setReloadTick] = useState(0)\n\n const reload = useCallback((): void => { setReloadTick(tick => tick + 1) }, [])\n\n useEffect(() => {\n let cancelled = false\n const qs = new URLSearchParams({\n scope, status, kind, limit: String(PAGE_SIZE), offset: String(offset),\n })\n if (q !== '') qs.set('q', q)\n api<ListResult>(`list?${qs.toString()}`)\n .then((data) => { if (!cancelled) { setList(data); setError(null) } })\n .catch((loadError: Error) => { if (!cancelled) setError(loadError.message) })\n api<{ parts: StatsPart[] }>('stats')\n .then((data) => { if (!cancelled) setStats(data.parts) })\n .catch(() => { /* 统计失败不影响列表 */ })\n return () => { cancelled = true }\n }, [scope, status, kind, q, offset, reloadTick])\n\n const act = (route: string, record: MemoryRow): void => {\n api(route, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: record.id, scope: record.scope }),\n })\n .then(reload)\n .catch((actError: Error) => { alert(actError.message) })\n }\n\n const page = Math.floor(offset / PAGE_SIZE) + 1\n const pages = list === null ? 1 : Math.max(1, Math.ceil(list.total / PAGE_SIZE))\n\n return (\n <div className={styles.panel}>\n <div className={styles.toolbar}>\n <div className={styles.scopeTabs}>\n {(['user', 'project'] as const).map(option => (\n <button key={option} type=\"button\"\n className={scope === option ? styles.scopeOn : styles.scopeOff}\n onClick={() => { setScope(option); setOffset(0) }}>\n {option === 'user' ? '用户级' : '项目级'}\n </button>\n ))}\n </div>\n <button type=\"button\" className={styles.button} onClick={reload}>刷新</button>\n <button type=\"button\" className={styles.button}\n onClick={() => { location.href = `/api/engram/export?scope=${scope}&format=markdown` }}>导出 MD</button>\n <button type=\"button\" className={styles.button}\n onClick={() => { location.href = `/api/engram/export?scope=${scope}&format=json` }}>导出 JSON</button>\n </div>\n <div className={styles.cards}>\n {(stats ?? []).map(part => (\n <button key={part.scope} type=\"button\" className={styles.card}\n style={part.scope === scope ? { borderColor: 'var(--accent)' } : undefined}\n onClick={() => { setScope(part.scope); setOffset(0) }}>\n <b>{part.stats.total}</b>\n <span>{part.scope} · active {part.stats.active} · 信噪比 {Math.round(part.stats.signalRatio * 100)}%</span>\n </button>\n ))}\n </div>\n <div className={styles.filters}>\n <select className={styles.input} value={status}\n onChange={event => { setStatus(event.target.value); setOffset(0) }}>\n <option value=\"all\">全部状态</option>\n <option value=\"active\">active</option>\n <option value=\"archived\">archived</option>\n <option value=\"forgotten\">forgotten</option>\n </select>\n <select className={styles.input} value={kind}\n onChange={event => { setKind(event.target.value); setOffset(0) }}>\n <option value=\"all\">全部种类</option>\n {KINDS.map(option => <option key={option} value={option}>{option}</option>)}\n </select>\n <input className={styles.input} placeholder=\"按内容搜索…\" value={q}\n onChange={event => { setQ(event.target.value.trim()); setOffset(0) }} />\n </div>\n {error !== null && <div className={styles.empty}>{`加载失败:${error}`}</div>}\n {error === null && list !== null && list.records.length === 0 && (\n <div className={styles.empty}>没有符合条件的记忆</div>\n )}\n {(list?.records ?? []).map(record => (\n <div key={record.id} className={styles.item}>\n <div className={styles.row1}>\n <span className={`${styles.badge} ${styles[record.status]}`}>{record.status}</span>\n <span className={styles.badge}>{record.kind}</span>\n <span className={styles.badge}>{record.scope}</span>\n </div>\n <div className={styles.content}>{record.content}</div>\n <div className={styles.meta}>\n <span>重要性 {record.importance.toFixed(2)}</span>\n <span>置信 {record.confidence.toFixed(2)}</span>\n <span>访问 {record.accessCount} 次</span>\n <span>{record.sourceSessionId === null\n ? '显式保存'\n : `来源 ${record.sourceSessionId.slice(0, 16)}…${record.sourceRound === null ? '' : ` 第${String(record.sourceRound)}轮`}`}</span>\n <span>{fmtTime(record.createdAt)}</span>\n </div>\n <div className={styles.ops}>\n <button type=\"button\" className={styles.button}\n onClick={() => { setReviewId(record) }}>详情</button>\n <button type=\"button\" className={styles.button}\n onClick={() => { setEditing(record) }}>编辑</button>\n {record.status === 'active'\n ? <button type=\"button\" className={styles.button}\n onClick={() => { act('forget', record) }}>遗忘</button>\n : <button type=\"button\" className={styles.button}\n onClick={() => { act('restore', record) }}>恢复</button>}\n </div>\n </div>\n ))}\n <div className={styles.pager}>\n <button type=\"button\" className={styles.button} disabled={offset === 0}\n onClick={() => { setOffset(Math.max(0, offset - PAGE_SIZE)) }}>上一页</button>\n <span>第 {page} / {pages} 页 · 共 {list?.total ?? 0} 条</span>\n <button type=\"button\" className={styles.button}\n disabled={list === null || offset + PAGE_SIZE >= list.total}\n onClick={() => { setOffset(offset + PAGE_SIZE) }}>下一页</button>\n </div>\n {editing !== null && (\n <EditDialog record={editing} onClose={() => { setEditing(null) }} onSaved={() => { setEditing(null); reload() }} />\n )}\n {reviewId !== null && (\n <div className={styles.backdrop} onClick={() => { setReviewId(null) }}>\n <div className={styles.dialog} onClick={event => event.stopPropagation()}>\n <h3>记忆审计</h3>\n <ReviewBody recordId={reviewId.id} scope={reviewId.scope} />\n <div className={styles.modalFoot}>\n <button type=\"button\" className={styles.button}\n onClick={() => { setReviewId(null) }}>关闭</button>\n </div>\n </div>\n </div>\n )}\n </div>\n )\n}\n","/**\n * dsh-engram client half: 注册设置页「记忆库」section。\n * 数据经回环 API(/api/engram/*)读取与操作,与 host 半的路由对齐。\n * @module @kenz1117/dsh-engram/client\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only: ctx.slots(renderer 的 SlotMap 渲染接线)。\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\n// Type-only: settings.section 槽位的 SlotMap merge(ui-settings 契约)。\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { EngramSection } from './EngramPanel.tsx'\n\n/** 必需服务:slots 渲染接线。 */\nexport const inject = ['slots']\n\n/**\n * Client plugin body: 设置页注册「记忆库」section(order 20,排在\n * general/models/plugins 之后)。\n * @param ctx - client root context.\n */\nexport function apply(ctx: Context): void {\n ctx.slots.inject('settings.section', () => ctx.slots.register({\n name: 'settings.section',\n id: 'engram',\n order: 20,\n label: () => '记忆库',\n }, EngramSection))\n}\n"],"mappings":"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"}
|
|
@@ -374,308 +374,6 @@ async function ingestPreviousTurn(deps) {
|
|
|
374
374
|
};
|
|
375
375
|
}
|
|
376
376
|
//#endregion
|
|
377
|
-
//#region src/page.ts
|
|
378
|
-
/**
|
|
379
|
-
* /engram 管理页:自包含单文件 HTML(内联 CSS/JS,简体中文 UI)。
|
|
380
|
-
* 页面 JS 全部使用 DOM API 构建用户内容节点(不用 innerHTML 拼接,
|
|
381
|
-
* 防记忆内容 XSS)。@module @kenz1117/dsh-engram/page
|
|
382
|
-
*/
|
|
383
|
-
const PAGE_HTML = `<!doctype html>
|
|
384
|
-
<html lang="zh-CN">
|
|
385
|
-
<head>
|
|
386
|
-
<meta charset="utf-8">
|
|
387
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
388
|
-
<title>dsh-engram 记忆库</title>
|
|
389
|
-
<style>
|
|
390
|
-
:root { color-scheme: dark; --bg: #14161a; --panel: #1c1f26; --line: #2a2f3a; --text: #e6e8ee; --muted: #8b93a3; --accent: #4f8cff; --ok: #3fb96f; --warn: #d9a13b; --bad: #e05d5d; }
|
|
391
|
-
* { box-sizing: border-box; }
|
|
392
|
-
body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.6 -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; }
|
|
393
|
-
header { display: flex; align-items: center; gap: 12px; padding: 14px 20px; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--bg); z-index: 5; }
|
|
394
|
-
header h1 { font-size: 16px; margin: 0 auto 0 0; }
|
|
395
|
-
.tabs { display: flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
|
396
|
-
.tabs button { background: none; border: none; color: var(--muted); padding: 6px 14px; cursor: pointer; font-size: 13px; }
|
|
397
|
-
.tabs button.on { background: var(--accent); color: #fff; }
|
|
398
|
-
button.act { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 6px 12px; cursor: pointer; font-size: 13px; }
|
|
399
|
-
button.act:hover { border-color: var(--accent); }
|
|
400
|
-
main { max-width: 980px; margin: 0 auto; padding: 18px 20px 60px; }
|
|
401
|
-
.cards { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-bottom: 16px; }
|
|
402
|
-
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 10px 14px; }
|
|
403
|
-
.card b { display: block; font-size: 20px; }
|
|
404
|
-
.card span { color: var(--muted); font-size: 12px; }
|
|
405
|
-
.filters { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
|
|
406
|
-
.filters select, .filters input { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 6px 10px; font-size: 13px; }
|
|
407
|
-
.filters input { flex: 1; min-width: 200px; }
|
|
408
|
-
.item { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin-bottom: 10px; }
|
|
409
|
-
.item .row1 { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; flex-wrap: wrap; }
|
|
410
|
-
.badge { font-size: 11px; padding: 1px 8px; border-radius: 99px; border: 1px solid var(--line); color: var(--muted); }
|
|
411
|
-
.badge.active { color: var(--ok); border-color: var(--ok); }
|
|
412
|
-
.badge.archived { color: var(--warn); border-color: var(--warn); }
|
|
413
|
-
.badge.forgotten { color: var(--bad); border-color: var(--bad); }
|
|
414
|
-
.content { white-space: pre-wrap; word-break: break-word; }
|
|
415
|
-
.meta { color: var(--muted); font-size: 12px; margin-top: 6px; display: flex; gap: 14px; flex-wrap: wrap; }
|
|
416
|
-
.ops { margin-top: 8px; display: flex; gap: 8px; }
|
|
417
|
-
.ops button { font-size: 12px; padding: 3px 10px; }
|
|
418
|
-
.bar { height: 4px; border-radius: 2px; background: var(--line); overflow: hidden; width: 90px; display: inline-block; vertical-align: middle; }
|
|
419
|
-
.bar i { display: block; height: 100%; background: var(--accent); }
|
|
420
|
-
.pager { display: flex; gap: 10px; align-items: center; justify-content: center; margin-top: 14px; color: var(--muted); }
|
|
421
|
-
dialog { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 12px; max-width: 640px; width: 92vw; padding: 18px; }
|
|
422
|
-
dialog::backdrop { background: rgba(0,0,0,.55); }
|
|
423
|
-
dialog h3 { margin: 0 0 10px; font-size: 15px; }
|
|
424
|
-
dialog textarea, dialog input, dialog select { width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 8px; font: inherit; }
|
|
425
|
-
dialog .f { margin-bottom: 10px; }
|
|
426
|
-
dialog label { display: block; color: var(--muted); font-size: 12px; margin-bottom: 4px; }
|
|
427
|
-
dialog pre { background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 10px; overflow: auto; font-size: 12px; max-height: 320px; white-space: pre-wrap; }
|
|
428
|
-
.modal-foot { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
|
|
429
|
-
.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
430
|
-
.empty { text-align: center; color: var(--muted); padding: 40px 0; }
|
|
431
|
-
</style>
|
|
432
|
-
</head>
|
|
433
|
-
<body>
|
|
434
|
-
<header>
|
|
435
|
-
<h1>dsh-engram 记忆库</h1>
|
|
436
|
-
<div class="tabs" id="scopeTabs">
|
|
437
|
-
<button data-scope="user" class="on">用户级</button>
|
|
438
|
-
<button data-scope="project">项目级</button>
|
|
439
|
-
</div>
|
|
440
|
-
<button class="act" id="refresh">刷新</button>
|
|
441
|
-
<button class="act" id="expMd">导出 MD</button>
|
|
442
|
-
<button class="act" id="expJson">导出 JSON</button>
|
|
443
|
-
</header>
|
|
444
|
-
<main>
|
|
445
|
-
<div class="cards" id="cards"></div>
|
|
446
|
-
<div class="filters">
|
|
447
|
-
<select id="fStatus">
|
|
448
|
-
<option value="all">全部状态</option>
|
|
449
|
-
<option value="active">active</option>
|
|
450
|
-
<option value="archived">archived</option>
|
|
451
|
-
<option value="forgotten">forgotten</option>
|
|
452
|
-
</select>
|
|
453
|
-
<select id="fKind">
|
|
454
|
-
<option value="all">全部种类</option>
|
|
455
|
-
<option value="fact">fact</option>
|
|
456
|
-
<option value="preference">preference</option>
|
|
457
|
-
<option value="decision">decision</option>
|
|
458
|
-
<option value="episode">episode</option>
|
|
459
|
-
<option value="skill">skill</option>
|
|
460
|
-
</select>
|
|
461
|
-
<input id="fQ" placeholder="按内容搜索…">
|
|
462
|
-
</div>
|
|
463
|
-
<div id="list"></div>
|
|
464
|
-
<div class="pager">
|
|
465
|
-
<button class="act" id="prev">上一页</button>
|
|
466
|
-
<span id="pageInfo"></span>
|
|
467
|
-
<button class="act" id="next">下一页</button>
|
|
468
|
-
</div>
|
|
469
|
-
</main>
|
|
470
|
-
<dialog id="editDlg">
|
|
471
|
-
<h3>编辑记忆(写入取代链)</h3>
|
|
472
|
-
<div class="f"><label>内容</label><textarea id="eContent" rows="3"></textarea></div>
|
|
473
|
-
<div class="f"><label>种类</label>
|
|
474
|
-
<select id="eKind">
|
|
475
|
-
<option value="fact">fact</option><option value="preference">preference</option>
|
|
476
|
-
<option value="decision">decision</option><option value="episode">episode</option>
|
|
477
|
-
<option value="skill">skill</option>
|
|
478
|
-
</select>
|
|
479
|
-
</div>
|
|
480
|
-
<div class="f"><label>重要性:<span id="eImpV">0.5</span></label>
|
|
481
|
-
<input type="range" id="eImp" min="0" max="1" step="0.05" value="0.5">
|
|
482
|
-
</div>
|
|
483
|
-
<div class="modal-foot">
|
|
484
|
-
<button class="act" id="eCancel">取消</button>
|
|
485
|
-
<button class="act primary" id="eSave">保存(旧条目归档)</button>
|
|
486
|
-
</div>
|
|
487
|
-
</dialog>
|
|
488
|
-
<dialog id="reviewDlg">
|
|
489
|
-
<h3>记忆审计</h3>
|
|
490
|
-
<pre id="rBody"></pre>
|
|
491
|
-
<div class="modal-foot"><button class="act" id="rClose">关闭</button></div>
|
|
492
|
-
</dialog>
|
|
493
|
-
<script>
|
|
494
|
-
(function () {
|
|
495
|
-
'use strict';
|
|
496
|
-
var state = { scope: 'user', status: 'all', kind: 'all', q: '', limit: 20, offset: 0, total: 0 };
|
|
497
|
-
var $ = function (id) { return document.getElementById(id); };
|
|
498
|
-
|
|
499
|
-
function el(tag, cls, text) {
|
|
500
|
-
var node = document.createElement(tag);
|
|
501
|
-
if (cls) node.className = cls;
|
|
502
|
-
if (text !== undefined) node.textContent = text;
|
|
503
|
-
return node;
|
|
504
|
-
}
|
|
505
|
-
function api(path, opts) {
|
|
506
|
-
return fetch('/api/engram/' + path, opts).then(function (res) {
|
|
507
|
-
return res.json().then(function (body) {
|
|
508
|
-
if (!res.ok) throw new Error(body && body.error ? body.error : ('HTTP ' + res.status));
|
|
509
|
-
return body;
|
|
510
|
-
});
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
function fmtTime(ms) { return new Date(ms).toLocaleString('zh-CN', { hour12: false }); }
|
|
514
|
-
function bar(v) {
|
|
515
|
-
var wrap = el('span', 'bar');
|
|
516
|
-
var fill = el('i');
|
|
517
|
-
fill.style.width = Math.round(v * 100) + '%';
|
|
518
|
-
wrap.appendChild(fill);
|
|
519
|
-
return wrap;
|
|
520
|
-
}
|
|
521
|
-
function loadStats() {
|
|
522
|
-
return api('stats').then(function (data) {
|
|
523
|
-
var cards = $('cards');
|
|
524
|
-
cards.textContent = '';
|
|
525
|
-
data.parts.forEach(function (part) {
|
|
526
|
-
var s = part.stats;
|
|
527
|
-
var on = part.scope === state.scope;
|
|
528
|
-
var card = el('div', 'card');
|
|
529
|
-
if (on) card.style.borderColor = 'var(--accent)';
|
|
530
|
-
card.style.cursor = 'pointer';
|
|
531
|
-
card.appendChild(el('b', undefined, String(s.total)));
|
|
532
|
-
card.appendChild(el('span', undefined, part.scope + ' · active ' + s.active + ' · 信噪比 ' + Math.round(s.signalRatio * 100) + '%'));
|
|
533
|
-
card.addEventListener('click', function () { setScope(part.scope); });
|
|
534
|
-
cards.appendChild(card);
|
|
535
|
-
});
|
|
536
|
-
}).catch(function (e) { console.warn(e); });
|
|
537
|
-
}
|
|
538
|
-
function loadList() {
|
|
539
|
-
var qs = 'scope=' + state.scope + '&status=' + state.status + '&kind=' + state.kind
|
|
540
|
-
+ '&limit=' + state.limit + '&offset=' + state.offset
|
|
541
|
-
+ (state.q ? '&q=' + encodeURIComponent(state.q) : '');
|
|
542
|
-
return api('list?' + qs).then(function (data) {
|
|
543
|
-
state.total = data.total;
|
|
544
|
-
var list = $('list');
|
|
545
|
-
list.textContent = '';
|
|
546
|
-
if (data.records.length === 0) { list.appendChild(el('div', 'empty', '没有符合条件的记忆')); }
|
|
547
|
-
data.records.forEach(function (record) {
|
|
548
|
-
var item = el('div', 'item');
|
|
549
|
-
var row1 = el('div', 'row1');
|
|
550
|
-
row1.appendChild(el('span', 'badge ' + record.status, record.status));
|
|
551
|
-
row1.appendChild(el('span', 'badge', record.kind));
|
|
552
|
-
row1.appendChild(el('span', 'badge', record.scope));
|
|
553
|
-
item.appendChild(row1);
|
|
554
|
-
item.appendChild(el('div', 'content', record.content));
|
|
555
|
-
var meta = el('div', 'meta');
|
|
556
|
-
meta.appendChild(el('span', undefined, '重要性 ' + record.importance.toFixed(2) + ' '));
|
|
557
|
-
meta.appendChild(bar(record.importance));
|
|
558
|
-
meta.appendChild(el('span', undefined, '置信 ' + record.confidence.toFixed(2) + ' '));
|
|
559
|
-
meta.appendChild(bar(record.confidence));
|
|
560
|
-
meta.appendChild(el('span', undefined, '访问 ' + record.accessCount + ' 次'));
|
|
561
|
-
var source = record.sourceSessionId
|
|
562
|
-
? '来源 ' + record.sourceSessionId.slice(0, 16) + '…' + (record.sourceRound === null ? '' : ' 第' + record.sourceRound + '轮')
|
|
563
|
-
: '显式保存';
|
|
564
|
-
meta.appendChild(el('span', undefined, source));
|
|
565
|
-
meta.appendChild(el('span', undefined, fmtTime(record.createdAt)));
|
|
566
|
-
item.appendChild(meta);
|
|
567
|
-
var ops = el('div', 'ops');
|
|
568
|
-
var reviewBtn = el('button', 'act', '详情');
|
|
569
|
-
reviewBtn.addEventListener('click', function () { openReview(record); });
|
|
570
|
-
var editBtn = el('button', 'act', '编辑');
|
|
571
|
-
editBtn.addEventListener('click', function () { openEdit(record); });
|
|
572
|
-
ops.appendChild(reviewBtn);
|
|
573
|
-
ops.appendChild(editBtn);
|
|
574
|
-
if (record.status === 'active') {
|
|
575
|
-
var forgetBtn = el('button', 'act', '遗忘');
|
|
576
|
-
forgetBtn.addEventListener('click', function () { post('forget', record, '已遗忘(可恢复)'); });
|
|
577
|
-
ops.appendChild(forgetBtn);
|
|
578
|
-
} else {
|
|
579
|
-
var restoreBtn = el('button', 'act', '恢复');
|
|
580
|
-
restoreBtn.addEventListener('click', function () { post('restore', record, '已恢复为 active'); });
|
|
581
|
-
ops.appendChild(restoreBtn);
|
|
582
|
-
}
|
|
583
|
-
item.appendChild(ops);
|
|
584
|
-
list.appendChild(item);
|
|
585
|
-
});
|
|
586
|
-
var page = Math.floor(state.offset / state.limit) + 1;
|
|
587
|
-
var pages = Math.max(1, Math.ceil(state.total / state.limit));
|
|
588
|
-
$('pageInfo').textContent = '第 ' + page + ' / ' + pages + ' 页 · 共 ' + state.total + ' 条';
|
|
589
|
-
$('prev').disabled = state.offset === 0;
|
|
590
|
-
$('next').disabled = state.offset + state.limit >= state.total;
|
|
591
|
-
}).catch(function (e) {
|
|
592
|
-
$('list').textContent = '';
|
|
593
|
-
$('list').appendChild(el('div', 'empty', '加载失败:' + e.message));
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
function reload() { loadStats(); loadList(); }
|
|
597
|
-
function setScope(scope) {
|
|
598
|
-
state.scope = scope; state.offset = 0;
|
|
599
|
-
document.querySelectorAll('#scopeTabs button').forEach(function (b) {
|
|
600
|
-
b.classList.toggle('on', b.dataset.scope === scope);
|
|
601
|
-
});
|
|
602
|
-
reload();
|
|
603
|
-
}
|
|
604
|
-
function post(route, record, okText) {
|
|
605
|
-
api(route, {
|
|
606
|
-
method: 'POST',
|
|
607
|
-
headers: { 'content-type': 'application/json' },
|
|
608
|
-
body: JSON.stringify({ id: record.id, scope: record.scope }),
|
|
609
|
-
}).then(function () { reload(); }).catch(function (e) { alert(e.message); });
|
|
610
|
-
}
|
|
611
|
-
function openReview(record) {
|
|
612
|
-
api('review?scope=' + record.scope + '&id=' + encodeURIComponent(record.id)).then(function (view) {
|
|
613
|
-
var r = view.record;
|
|
614
|
-
var lines = [
|
|
615
|
-
'内容: ' + r.content,
|
|
616
|
-
'属性: kind=' + r.kind + ', status=' + r.status + ', importance=' + r.importance + ', confidence=' + r.confidence + ', 访问 ' + r.accessCount + ' 次',
|
|
617
|
-
'来源: ' + (r.sourceSessionId ? r.sourceSessionId + (r.sourceRound === null ? '' : ' 第' + r.sourceRound + '轮') + (r.sourceSeq === null ? '' : ' seq ' + r.sourceSeq) : '显式保存'),
|
|
618
|
-
];
|
|
619
|
-
if (view.supersededBy.length) lines.push('被谁取代: ' + view.supersededBy.join(', '));
|
|
620
|
-
if (view.supersedes.length) lines.push('取代了谁: ' + view.supersedes.join(', '));
|
|
621
|
-
if (view.contradicts.length) lines.push('矛盾候选: ' + view.contradicts.join(', '));
|
|
622
|
-
if (view.related.length) lines.push('关联: ' + view.related.join(', '));
|
|
623
|
-
if (view.operations.length) {
|
|
624
|
-
lines.push('最近操作:');
|
|
625
|
-
view.operations.forEach(function (op) {
|
|
626
|
-
lines.push(' ' + fmtTime(op.at) + ' ' + op.op + (op.detail ? ' ' + op.detail : ''));
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
$('rBody').textContent = lines.join('\\n');
|
|
630
|
-
$('reviewDlg').showModal();
|
|
631
|
-
}).catch(function (e) { alert(e.message); });
|
|
632
|
-
}
|
|
633
|
-
function openEdit(record) {
|
|
634
|
-
$('eContent').value = record.content;
|
|
635
|
-
$('eKind').value = record.kind;
|
|
636
|
-
$('eImp').value = record.importance;
|
|
637
|
-
$('eImpV').textContent = record.importance.toFixed(2);
|
|
638
|
-
$('eSave').onclick = function () {
|
|
639
|
-
api('update', {
|
|
640
|
-
method: 'POST',
|
|
641
|
-
headers: { 'content-type': 'application/json' },
|
|
642
|
-
body: JSON.stringify({
|
|
643
|
-
id: record.id, scope: record.scope,
|
|
644
|
-
content: $('eContent').value, kind: $('eKind').value,
|
|
645
|
-
importance: Number($('eImp').value),
|
|
646
|
-
}),
|
|
647
|
-
}).then(function () { $('editDlg').close(); reload(); })
|
|
648
|
-
.catch(function (e) { alert(e.message); });
|
|
649
|
-
};
|
|
650
|
-
$('editDlg').showModal();
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
document.querySelectorAll('#scopeTabs button').forEach(function (b) {
|
|
654
|
-
b.addEventListener('click', function () { setScope(b.dataset.scope); });
|
|
655
|
-
});
|
|
656
|
-
$('refresh').addEventListener('click', reload);
|
|
657
|
-
$('expMd').addEventListener('click', function () { location.href = '/api/engram/export?scope=' + state.scope + '&format=markdown'; });
|
|
658
|
-
$('expJson').addEventListener('click', function () { location.href = '/api/engram/export?scope=' + state.scope + '&format=json'; });
|
|
659
|
-
$('fStatus').addEventListener('change', function (e) { state.status = e.target.value; state.offset = 0; loadList(); });
|
|
660
|
-
$('fKind').addEventListener('change', function (e) { state.kind = e.target.value; state.offset = 0; loadList(); });
|
|
661
|
-
var qTimer = null;
|
|
662
|
-
$('fQ').addEventListener('input', function (e) {
|
|
663
|
-
clearTimeout(qTimer);
|
|
664
|
-
qTimer = setTimeout(function () { state.q = e.target.value.trim(); state.offset = 0; loadList(); }, 300);
|
|
665
|
-
});
|
|
666
|
-
$('prev').addEventListener('click', function () { state.offset = Math.max(0, state.offset - state.limit); loadList(); });
|
|
667
|
-
$('next').addEventListener('click', function () { state.offset += state.limit; loadList(); });
|
|
668
|
-
$('eImp').addEventListener('input', function (e) { $('eImpV').textContent = Number(e.target.value).toFixed(2); });
|
|
669
|
-
$('eCancel').addEventListener('click', function () { $('editDlg').close(); });
|
|
670
|
-
$('rClose').addEventListener('click', function () { $('reviewDlg').close(); });
|
|
671
|
-
|
|
672
|
-
reload();
|
|
673
|
-
})();
|
|
674
|
-
<\/script>
|
|
675
|
-
</body>
|
|
676
|
-
</html>
|
|
677
|
-
`;
|
|
678
|
-
//#endregion
|
|
679
377
|
//#region src/routes.ts
|
|
680
378
|
/** 回环 peer:IPv4 127/8、IPv6 ::1、IPv4-mapped IPv6。 */
|
|
681
379
|
function isLoopbackPeer(req) {
|
|
@@ -753,15 +451,6 @@ function scopeOf$1(raw, fallback) {
|
|
|
753
451
|
* @param ctx - 携带 webServer 服务的宿主上下文。
|
|
754
452
|
*/
|
|
755
453
|
function registerEngramRoutes(ctx, deps) {
|
|
756
|
-
ctx.effect(() => ctx.webServer.register({
|
|
757
|
-
kind: "exact",
|
|
758
|
-
path: "/engram",
|
|
759
|
-
handler: async (req, res) => {
|
|
760
|
-
if (!guardLoopback(req, res)) return;
|
|
761
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
762
|
-
res.end(PAGE_HTML);
|
|
763
|
-
}
|
|
764
|
-
}), "dsh-engram: page route");
|
|
765
454
|
ctx.effect(() => ctx.webServer.register({
|
|
766
455
|
kind: "prefix",
|
|
767
456
|
path: "/api/engram",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenz1117/dsh-engram",
|
|
3
3
|
"description": "Cross-session long-term memory for DeepSeek Harness: dual-scope SQLite memory graph, hybrid retrieval, provenance audit, and a knowledge flywheel.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
"url": "git+https://github.com/kenz1117/dsh-engram.git"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
|
-
"main": "./lib/index.
|
|
14
|
-
"types": "./lib/index.d.mts",
|
|
13
|
+
"main": "./lib/index.js",
|
|
15
14
|
"exports": {
|
|
16
15
|
".": {
|
|
17
|
-
"
|
|
18
|
-
|
|
16
|
+
"default": "./lib/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./client": {
|
|
19
|
+
"default": "./lib/client.js"
|
|
19
20
|
},
|
|
20
21
|
"./src/*": "./src/*",
|
|
21
22
|
"./package.json": "./package.json"
|
|
@@ -28,6 +29,10 @@
|
|
|
28
29
|
"dsh": {
|
|
29
30
|
"bundle": {
|
|
30
31
|
"patch": "./cordis.patch.yml"
|
|
32
|
+
},
|
|
33
|
+
"client": {
|
|
34
|
+
"inject": [],
|
|
35
|
+
"platform": "web"
|
|
31
36
|
}
|
|
32
37
|
},
|
|
33
38
|
"scripts": {
|
|
@@ -54,6 +59,9 @@
|
|
|
54
59
|
},
|
|
55
60
|
"devDependencies": {
|
|
56
61
|
"@types/node": "^26.4.0",
|
|
62
|
+
"@types/react": "^18.3.12",
|
|
63
|
+
"@types/react-dom": "^18.3.1",
|
|
64
|
+
"lightningcss": "^1.32.0",
|
|
57
65
|
"tsdown": "^0.22.14",
|
|
58
66
|
"typescript": "^5.9.3",
|
|
59
67
|
"vitest": "^4.1.11"
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored tsdown client-bundle factory for OUT-OF-TREE (fork) UI plugins.
|
|
3
|
+
*
|
|
4
|
+
* The DSH monorepo publishes this shared preset at packages/client/tsdown.client.ts
|
|
5
|
+
* and expects in-tree packages to import it via a relative path. A standalone fork
|
|
6
|
+
* (or a fork cloned into packages/community/<name>) cannot resolve that shared file,
|
|
7
|
+
* so per the cookbook contract ("That preset is not published today, so a package
|
|
8
|
+
* outside this repository has to reproduce the same output format itself") this
|
|
9
|
+
* repository carries its own copy. Keep it in sync with the monorepo factory when
|
|
10
|
+
* the platform seed changes.
|
|
11
|
+
*
|
|
12
|
+
* Emits a closure-factory artifact: the bundle calls window.__ModuleLoader__.load({id, factory})
|
|
13
|
+
* and resolves externals through the injected require (loader module table —
|
|
14
|
+
* cordis DI entities, no globals, no import map). CSS Modules are compiled by
|
|
15
|
+
* lightningcss inside the bundle: importing `x.module.css` yields the
|
|
16
|
+
* hashed class map, and the css text auto-injects a <style data-plugin="<id>">
|
|
17
|
+
* tag at factory execution (the loader removes plugin-owned tags on unload).
|
|
18
|
+
* The virtual loader registers each real stylesheet as a watch dependency.
|
|
19
|
+
*/
|
|
20
|
+
import { readFile } from 'node:fs/promises'
|
|
21
|
+
import { existsSync } from 'node:fs'
|
|
22
|
+
import { basename, dirname, relative, resolve as resolvePath, sep } from 'node:path'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
import type { UserConfig } from 'tsdown'
|
|
25
|
+
import { transform } from 'lightningcss'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The host's eight-entry platform seed, copied verbatim from packages/client/web/src/platform.ts.
|
|
29
|
+
* These are the specifiers the loader module table answers at runtime, so the browser
|
|
30
|
+
* bundle keeps them external (require) instead of inlining. If the monorepo seed grows,
|
|
31
|
+
* mirror it here.
|
|
32
|
+
*/
|
|
33
|
+
const PLATFORM_MODULES = [
|
|
34
|
+
'react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', '@deepseek-ai/cordis',
|
|
35
|
+
'@deepseek-ai/dsh-client-store',
|
|
36
|
+
'@deepseek-ai/dsh-client-ui-slots',
|
|
37
|
+
'@deepseek-ai/dsh-client-ui-primitives',
|
|
38
|
+
] as const
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Virtual-id wrapper keeping module CSS away from tsdown's own css pipeline
|
|
42
|
+
* (which requires @tsdown/css). The suffix matters: tsdown's guard matches ids
|
|
43
|
+
* ending in `.css`, so the virtual id must not.
|
|
44
|
+
*/
|
|
45
|
+
const CSS_VIRTUAL_PREFIX = '\0dsh-css:'
|
|
46
|
+
const CSS_VIRTUAL_SUFFIX = '.mjs'
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Wire/type layers a client bundle may inline: browser-safe contracts
|
|
50
|
+
* with no runtime identity to share (no Symbol/instanceof/singleton state).
|
|
51
|
+
* Everything else under @deepseek-ai/* is either a module-table entry
|
|
52
|
+
* (external) or a leak the purity gate rejects.
|
|
53
|
+
*/
|
|
54
|
+
export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|session|llm|tools|brand)(\/|$)/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Vendored framework libraries: rescoped into @deepseek-ai, so the gate below
|
|
58
|
+
* would read them as plugin packages. They carry no cross-plugin runtime
|
|
59
|
+
* identity to share — the framework itself is a platform module (external),
|
|
60
|
+
* while these are ordinary libraries a browser bundle inlines.
|
|
61
|
+
*/
|
|
62
|
+
const VENDORED_LIBRARY = /^@deepseek-ai\/(cosmokit|schemastery)(\/|$)/
|
|
63
|
+
|
|
64
|
+
/** Generated descriptor/codec contribution with no shared runtime identity. */
|
|
65
|
+
const GENERATED_REMOTE = /^@deepseek-ai\/dsh-[a-z0-9]+(?:-[a-z0-9]+)*\/remote$/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Workspace mode replaces an empty config array with the root defaults. A
|
|
69
|
+
* falsey entry instead removes this package before entry resolution.
|
|
70
|
+
*/
|
|
71
|
+
const SKIP_WORKSPACE_BUILD: UserConfig = { entry: '' }
|
|
72
|
+
|
|
73
|
+
/** Externals resolved from the loader module table: the platform seed entries. */
|
|
74
|
+
export const CLIENT_EXTERNALS: readonly string[] = [...PLATFORM_MODULES]
|
|
75
|
+
|
|
76
|
+
// Repository root for sourcemap rebasing: this file lives at <repo>/scripts, so
|
|
77
|
+
// one level up is the fork root (the monorepo original walks up two levels
|
|
78
|
+
// because it sits at packages/client/).
|
|
79
|
+
const REPOSITORY_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
80
|
+
|
|
81
|
+
/** Rebase a physical lib-relative source onto a browser URL that mirrors the repository directories. */
|
|
82
|
+
function browserSourcePath(source: string, sourcemapPath: string): string {
|
|
83
|
+
if (!source.startsWith('.')) return source
|
|
84
|
+
const physicalSource = resolvePath(dirname(sourcemapPath), source)
|
|
85
|
+
const repositoryPath = relative(REPOSITORY_ROOT, physicalSource).split(sep).join('/')
|
|
86
|
+
return repositoryPath.startsWith('packages/') ? `../../../${repositoryPath}` : source
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build the tsdown config for one UI plugin package: the node-half lib build
|
|
91
|
+
* plus the browser client bundle. Client packages emit both halves during the
|
|
92
|
+
* Client pass by default; packages needed for Host reflection may opt into the
|
|
93
|
+
* earlier Host pass. A package-level tsdown.config.ts REPLACES the root
|
|
94
|
+
* workspace layout, so the lib half must be restated here — dropping it leaves
|
|
95
|
+
* the package without lib/index.js and the host Loader cannot import its node
|
|
96
|
+
* half.
|
|
97
|
+
* @param id - plugin id (package name), stamped into the __ModuleLoader__.load
|
|
98
|
+
* handoff and onto the injected style tags.
|
|
99
|
+
* @param libEntry - node-half entries, spelled at the call site so the
|
|
100
|
+
* package-invariants gate can see `lib/types/invariant.js` in each package's
|
|
101
|
+
* own tsdown.config.ts (a preset-side glob hides it from the mechanical check).
|
|
102
|
+
* @param options - phase placement, lib overrides, and companion Node configs.
|
|
103
|
+
* @returns ENV-selected tsdown config for the current build face.
|
|
104
|
+
*/
|
|
105
|
+
export function clientBundle(
|
|
106
|
+
id: string,
|
|
107
|
+
libEntry: readonly string[],
|
|
108
|
+
options: ClientBundleOptions = {},
|
|
109
|
+
): BuildFaceConfig {
|
|
110
|
+
const lib = clientLibraryConfig(id, libEntry, options.lib)
|
|
111
|
+
return ({ env }) => {
|
|
112
|
+
const face = buildFace(env?.DSH_BUILD_FACE)
|
|
113
|
+
const client = clientConfig(id, face === undefined
|
|
114
|
+
? 'src/client/index.ts'
|
|
115
|
+
: 'lib/types/client/index.js')
|
|
116
|
+
const node = [lib, ...(options.companions ?? [])]
|
|
117
|
+
if (face === 'host') return options.hostPhase === true ? node : [SKIP_WORKSPACE_BUILD]
|
|
118
|
+
if (face === 'client') return options.hostPhase === true ? [client] : [...node, client]
|
|
119
|
+
return [...node, client]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build a Client-only Node library during the Client pass.
|
|
125
|
+
* @param id - Package name used in tsdown diagnostics.
|
|
126
|
+
* @param libEntry - Emitted JavaScript entries consumed from `lib/types`.
|
|
127
|
+
* @returns ENV-selected tsdown config for the Client build face.
|
|
128
|
+
*/
|
|
129
|
+
export function clientLibrary(id: string, libEntry: readonly string[]): BuildFaceConfig {
|
|
130
|
+
const lib = clientLibraryConfig(id, libEntry)
|
|
131
|
+
return clientOnly([lib])
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Select arbitrary package-local configs only during the Client pass.
|
|
136
|
+
* @param configs - Node-side configs emitted after Client tsc.
|
|
137
|
+
* @returns ENV-selected tsdown config for the Client build face.
|
|
138
|
+
*/
|
|
139
|
+
export function clientOnly(configs: readonly UserConfig[]): BuildFaceConfig {
|
|
140
|
+
return ({ env }) => buildFace(env?.DSH_BUILD_FACE) === 'host'
|
|
141
|
+
? [SKIP_WORKSPACE_BUILD]
|
|
142
|
+
: [...configs]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface ClientBundleOptions {
|
|
146
|
+
/** Emit the Node-side artifacts during the Host pass instead of the Client pass. */
|
|
147
|
+
readonly hostPhase?: boolean
|
|
148
|
+
/** Additional Node-side configs emitted alongside the package library. */
|
|
149
|
+
readonly companions?: readonly UserConfig[]
|
|
150
|
+
/** Overrides for the package's primary Node-side library config. */
|
|
151
|
+
readonly lib?: UserConfig
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type BuildFace = 'host' | 'client' | undefined
|
|
155
|
+
|
|
156
|
+
type BuildFaceConfig = (inlineConfig: Pick<UserConfig, 'env'>) => UserConfig[]
|
|
157
|
+
|
|
158
|
+
function buildFace(value: unknown): BuildFace {
|
|
159
|
+
if (value === undefined || value === 'host' || value === 'client') return value
|
|
160
|
+
throw new Error(`tsdown: --env.DSH_BUILD_FACE must be host or client, received ${String(value)}`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function clientLibraryConfig(
|
|
164
|
+
id: string,
|
|
165
|
+
libEntry: readonly string[],
|
|
166
|
+
overrides: UserConfig = {},
|
|
167
|
+
): UserConfig {
|
|
168
|
+
return {
|
|
169
|
+
name: id,
|
|
170
|
+
entry: [...libEntry],
|
|
171
|
+
outDir: 'lib',
|
|
172
|
+
format: ['esm'],
|
|
173
|
+
platform: 'node',
|
|
174
|
+
target: 'es2024',
|
|
175
|
+
fixedExtension: false,
|
|
176
|
+
dts: false,
|
|
177
|
+
clean: false,
|
|
178
|
+
...overrides,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function clientConfig(id: string, entry: string): UserConfig {
|
|
183
|
+
return {
|
|
184
|
+
name: `${id}/client`,
|
|
185
|
+
entry: { client: entry },
|
|
186
|
+
// Browser bundle lands next to the node half (single lib/ artifact dir;
|
|
187
|
+
// the entryFileNames pin keeps it exactly lib/client.js). clean must stay
|
|
188
|
+
// off — a default clean would wipe the node-half output emitted above.
|
|
189
|
+
outDir: 'lib',
|
|
190
|
+
format: 'cjs',
|
|
191
|
+
platform: 'browser',
|
|
192
|
+
// Types ship from lib/types (tsc); dts here would wrap the banner/footer into .d.cts and break parsing.
|
|
193
|
+
dts: false,
|
|
194
|
+
// Plugin code is fetched outside Vite's module graph, so its own bundle
|
|
195
|
+
// must carry the TS/TSX mapping consumed by browser profiling tools.
|
|
196
|
+
sourcemap: true,
|
|
197
|
+
// DSH Store 的固定 Commit 审查对运行时文件有 256 KiB 单文件硬上限;
|
|
198
|
+
// 未压缩 bundle 含内联 CSS/双语字典远超此值,浏览器分发面统一压缩。
|
|
199
|
+
// (DSH Store fixed-commit review caps a single runtime file at 256 KiB;
|
|
200
|
+
// the uncompressed bundle inlines CSS and bilingual dictionaries, so ship
|
|
201
|
+
// the browser artifact minified.)
|
|
202
|
+
minify: true,
|
|
203
|
+
clean: false,
|
|
204
|
+
external: [...CLIENT_EXTERNALS],
|
|
205
|
+
// Browser bundles inline node-idiom deps (zustand/immer read
|
|
206
|
+
// process.env.NODE_ENV; zustand's esm build also probes
|
|
207
|
+
// import.meta.env.MODE, which a CJS output cannot carry — rolldown flags
|
|
208
|
+
// EMPTY_IMPORT_META). vite defined both on the seed path; tsdown inlining
|
|
209
|
+
// needs the substitutions here or the factory throws ReferenceError at
|
|
210
|
+
// boot / the build gate reds. Both keys honor the build's NODE_ENV so a
|
|
211
|
+
// dev build keeps the dev-branch semantics; artifacts default to production.
|
|
212
|
+
// The bare `import.meta.env` key is required alongside the precise MODE
|
|
213
|
+
// key: zustand probes `import.meta.env ? import.meta.env.MODE : ...`, and
|
|
214
|
+
// the truthiness probe would otherwise survive as an empty import.meta.
|
|
215
|
+
define: {
|
|
216
|
+
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'),
|
|
217
|
+
'import.meta.env.MODE': JSON.stringify(process.env.NODE_ENV ?? 'production'),
|
|
218
|
+
'import.meta.env': JSON.stringify({ MODE: process.env.NODE_ENV ?? 'production' }),
|
|
219
|
+
},
|
|
220
|
+
// tsdown auto-externalizes package dependencies; anything NOT in the
|
|
221
|
+
// loader module table must inline instead (wire/type layers, zod, clsx —
|
|
222
|
+
// every non-shared dep). A require() the table cannot answer is a
|
|
223
|
+
// guaranteed runtime throw, so the rule is the table list itself: no
|
|
224
|
+
// opinion for table entries (external above wins), bundle everything else.
|
|
225
|
+
noExternal: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
|
|
226
|
+
plugins: [{
|
|
227
|
+
// Bundle purity gate (build-time mirror of the module-edge rules):
|
|
228
|
+
// platform seed entries stay external, inline-safe wire layers inline,
|
|
229
|
+
// and every other @deepseek-ai value import is a build error — a
|
|
230
|
+
// cross-plugin value import either inlines a duplicate runtime instance
|
|
231
|
+
// or requires a specifier the frozen module table cannot answer.
|
|
232
|
+
// Cross-plugin collaboration goes through cordis services instead.
|
|
233
|
+
name: 'dsh-client-bundle-purity',
|
|
234
|
+
resolveId(source: string) {
|
|
235
|
+
if (!source.startsWith('@deepseek-ai/')) return null
|
|
236
|
+
if (CLIENT_EXTERNALS.includes(source)) return null // platform module: external wins
|
|
237
|
+
if (VENDORED_LIBRARY.test(source)) return null // vendored library: inline, no shared identity
|
|
238
|
+
if (INLINE_SAFE.test(source) || GENERATED_REMOTE.test(source)) return null // wire contribution: inline is the point
|
|
239
|
+
throw new Error(
|
|
240
|
+
`client bundle purity: "${source}" is not a platform module (CLIENT_EXTERNALS), an inline-safe wire layer, or a generated /remote contribution — `
|
|
241
|
+
+ 'cross-plugin value imports are forbidden; collaborate through cordis services (type-only imports are erased and never reach this gate)',
|
|
242
|
+
)
|
|
243
|
+
},
|
|
244
|
+
}, {
|
|
245
|
+
name: 'dsh-css-modules-inline',
|
|
246
|
+
resolveId(source: string, importer: string | undefined) {
|
|
247
|
+
if (!source.endsWith('.module.css')) return null
|
|
248
|
+
const abs = importer !== undefined ? sourceAssetPath(source, importer) : source
|
|
249
|
+
return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX
|
|
250
|
+
},
|
|
251
|
+
async load(virtualId: string) {
|
|
252
|
+
if (!virtualId.startsWith(CSS_VIRTUAL_PREFIX)) return null
|
|
253
|
+
const fileId = virtualId.slice(CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length)
|
|
254
|
+
// The virtual id otherwise hides the physical stylesheet from Rolldown's watch graph.
|
|
255
|
+
this.addWatchFile(fileId)
|
|
256
|
+
const source = await readFile(fileId)
|
|
257
|
+
const { code, exports: cssExports } = transform({
|
|
258
|
+
filename: fileId,
|
|
259
|
+
code: source,
|
|
260
|
+
cssModules: { pattern: '[hash]_[local]' },
|
|
261
|
+
minify: true,
|
|
262
|
+
})
|
|
263
|
+
const classMap: Record<string, string> = {}
|
|
264
|
+
for (const [local, exp] of Object.entries(cssExports ?? {})) classMap[local] = exp.name
|
|
265
|
+
// One <style data-plugin> per module file; idempotent under re-evaluation.
|
|
266
|
+
return [
|
|
267
|
+
`const css = ${JSON.stringify(code.toString())};`,
|
|
268
|
+
`const tagId = ${JSON.stringify(`${id}/${basename(fileId)}`)};`,
|
|
269
|
+
'if (typeof document !== \'undefined\' && document.querySelector(\'style[data-plugin-css=\' + JSON.stringify(tagId) + \']\') === null) {',
|
|
270
|
+
' const tag = document.createElement(\'style\');',
|
|
271
|
+
` tag.dataset.plugin = ${JSON.stringify(id)};`,
|
|
272
|
+
' tag.dataset.pluginCss = tagId;',
|
|
273
|
+
' tag.textContent = css;',
|
|
274
|
+
' document.head.appendChild(tag);',
|
|
275
|
+
'}',
|
|
276
|
+
`export default ${JSON.stringify(classMap)};`,
|
|
277
|
+
].join('\n')
|
|
278
|
+
},
|
|
279
|
+
}],
|
|
280
|
+
outputOptions: {
|
|
281
|
+
entryFileNames: 'client.js',
|
|
282
|
+
// The map is served from /plugins/<scoped-package>/client.js.map. The
|
|
283
|
+
// browser resolves its local sources back into URLs that mirror the
|
|
284
|
+
// /packages/<group>/<package>/src directories; sourcesContent keeps them usable
|
|
285
|
+
// without exposing that tree as an HTTP route.
|
|
286
|
+
sourcemapPathTransform: browserSourcePath,
|
|
287
|
+
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(id)}, factory: (require) => {`,
|
|
288
|
+
footer: 'return module.exports; } });',
|
|
289
|
+
intro: 'var module = { exports: {} }; var exports = module.exports;',
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Resolve an emitted JS asset import against its source-tree counterpart. */
|
|
295
|
+
function sourceAssetPath(source: string, importer: string): string {
|
|
296
|
+
const emitted = resolvePath(dirname(importer), source)
|
|
297
|
+
if (existsSync(emitted)) return emitted
|
|
298
|
+
const marker = `${sep}lib${sep}types${sep}`
|
|
299
|
+
const boundary = emitted.indexOf(marker)
|
|
300
|
+
if (boundary < 0) return emitted
|
|
301
|
+
return resolvePath(emitted.slice(0, boundary), 'src', emitted.slice(boundary + marker.length))
|
|
302
|
+
}
|
package/scripts/link-peers.mjs
CHANGED
|
@@ -29,6 +29,8 @@ const LINKED_PACKAGES = [
|
|
|
29
29
|
['@deepseek-ai/dsh-system-prompt', 'packages/core/system-prompt'],
|
|
30
30
|
['@deepseek-ai/dsh-session', 'packages/core/session'],
|
|
31
31
|
['@deepseek-ai/dsh-host-webserver', 'packages/host/webserver'],
|
|
32
|
+
['@deepseek-ai/dsh-client-ui-settings', 'packages/client/ui-settings'],
|
|
33
|
+
['@deepseek-ai/dsh-client-ui-renderer', 'packages/client/ui-renderer'],
|
|
32
34
|
['@deepseek-ai/dsh-llm', 'packages/llm/llm'],
|
|
33
35
|
['@deepseek-ai/dsh-agent', 'packages/core/agent'],
|
|
34
36
|
['@deepseek-ai/dsh-scope', 'packages/core/scope'],
|
package/lib/index.d.mts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { Context } from "@deepseek-ai/cordis";
|
|
3
|
-
//#region src/config.d.ts
|
|
4
|
-
/** 自动摄取档位:off 关闭;light 只读用户消息(每轮≤2 条);eager 用户+助手消息(每轮≤5 条)。 */
|
|
5
|
-
type IngestModeConfig = 'off' | 'light' | 'eager';
|
|
6
|
-
/** 插件配置。 */
|
|
7
|
-
interface EngramConfig {
|
|
8
|
-
/** 两个 SQLite 分库与嵌入模型缓存的根目录;默认 `~/.dsh/engram`。 */
|
|
9
|
-
dbDir?: string;
|
|
10
|
-
/** 会话开始是否注入用户级画像摘要;默认 true。 */
|
|
11
|
-
injectProfile?: boolean;
|
|
12
|
-
/** 画像注入的最大条数;默认 8。 */
|
|
13
|
-
profileTopN?: number;
|
|
14
|
-
/** 嵌入模型缓存目录;默认 `<dbDir>/models`。 */
|
|
15
|
-
modelCacheDir?: string;
|
|
16
|
-
/** 嵌入模型下载端点;默认 huggingface.co,网络受限环境配镜像(如 https://hf-mirror.com)。 */
|
|
17
|
-
hfEndpoint?: string;
|
|
18
|
-
/** 自动摄取档位;默认 off(显式开启才写库)。 */
|
|
19
|
-
ingest?: IngestModeConfig;
|
|
20
|
-
/** 蒸馏/摄取覆盖路由的 provider;必须与 model 成对提供。 */
|
|
21
|
-
provider?: string;
|
|
22
|
-
/** 蒸馏/摄取覆盖路由的 model;必须与 provider 成对提供。 */
|
|
23
|
-
model?: string;
|
|
24
|
-
/** 衰减:最近访问超过该天数才可能被归档;默认 30。 */
|
|
25
|
-
decayAfterDays?: number;
|
|
26
|
-
/** 衰减:importance 低于该值才可能被归档;默认 0.3。 */
|
|
27
|
-
decayImportanceBelow?: number;
|
|
28
|
-
}
|
|
29
|
-
/** Schemastery 校验面(cordis.yml 读取时校验)。 */
|
|
30
|
-
declare const Config: z<EngramConfig>;
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/index.d.ts
|
|
33
|
-
/** Cordis 插件名(loader 诊断与注入 source 使用)。 */
|
|
34
|
-
declare const name = "dsh-engram";
|
|
35
|
-
/** 必需服务:工具注册表与 LLM 流式端点(摄取/蒸馏的辅助调用)。 */
|
|
36
|
-
declare const inject: string[];
|
|
37
|
-
/** 会话开始注入的画像渲染:top-N 高重要性 user 记忆一行一条。 */
|
|
38
|
-
declare function renderProfile(topN: readonly {
|
|
39
|
-
kind: string;
|
|
40
|
-
content: string;
|
|
41
|
-
}[]): string;
|
|
42
|
-
/**
|
|
43
|
-
* 插件体:预热分库与嵌入器,注册 9 个工具、画像注入、自动摄取与衰减调度。
|
|
44
|
-
* @param ctx - host 上下文。
|
|
45
|
-
* @param config - cordis.yml 传入的可选配置;非法值在加载时 loud 失败。
|
|
46
|
-
*/
|
|
47
|
-
declare function apply(ctx: Context, config?: EngramConfig): void;
|
|
48
|
-
//#endregion
|
|
49
|
-
export { Config, type EngramConfig, apply, inject, name, renderProfile };
|