@oh-my-student/plugin-presets 0.0.0-stage → 0.1.1
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/COPYRIGHT +5 -0
- package/README.md +6 -2
- package/THIRD_PARTY_NOTICES.md +3 -0
- package/canvas-tool.js +56 -0
- package/catalog.js +34 -0
- package/drawing-tool.js +109 -0
- package/index.js +68 -0
- package/isolation.js +19 -0
- package/package.json +66 -4
- package/policy.js +98 -0
- package/prompt.js +44 -0
- package/resources/base.md +72 -0
- package/resources/companions/claude.md +5 -0
- package/resources/companions/deepseek.md +7 -0
- package/resources/companions/gemini.md +5 -0
- package/resources/companions/gpt.md +5 -0
- package/resources/coordinator.md +44 -0
- package/resources/manifest.json +41 -0
- package/resources/pi-skills-built/notara-examiner-analysis/SKILL.md +29 -0
- package/resources/pi-skills-built/notara-pdf-learning-set/SKILL.md +44 -0
- package/resources/pi-skills-built/notara-quest-orchestrator/SKILL.md +49 -0
- package/resources/pi-skills-built/notara-quest-tutor/SKILL.md +119 -0
- package/resources/pi-skills-built/notara-review-brainstorm/SKILL.md +34 -0
- package/resources/provenance.json +23 -0
- package/resources/workers/base.md +26 -0
- package/resources/workers/exercise.md +7 -0
- package/resources/workers/general.md +6 -0
- package/resources/workers/lesson.md +6 -0
- package/resources/workers/problem.md +7 -0
- package/resources/workers/review.md +6 -0
- package/resources.js +22 -0
- package/review-context.js +70 -0
- package/tools.js +118 -0
- package/topic-conversations.js +107 -0
package/COPYRIGHT
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (c) 2026 Notara contributors. All rights reserved.
|
|
2
|
+
|
|
3
|
+
No open-source license is granted for first-party Notara code or assets.
|
|
4
|
+
Publication does not grant permission to modify or redistribute them.
|
|
5
|
+
Third-party components remain governed by their respective licenses.
|
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @oh-my-student/plugin-presets
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
教学预设、提示词、学习技能和原生会话委派工具。
|
|
4
|
+
|
|
5
|
+
建议通过 `@oh-my-student/dsh-bundle@0.1.1` 安装完整插件组。要求 Node 24+、DSH 0.2.1-rc.4。保留版权,见 COPYRIGHT;第三方署名见 THIRD_PARTY_NOTICES.md。
|
|
6
|
+
|
|
7
|
+
配置与宿主适配说明:https://github.com/TongZi2003/Notara-Desktop/tree/main/notara-core/plugins/bundle
|
package/canvas-tool.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
3
|
+
import {drawingMathElement} from '@oh-my-student/plugin-vault-sqlite/math-reference';
|
|
4
|
+
import {parseDrawingDocument} from '@oh-my-student/plugin-vault-sqlite/drawing-files';
|
|
5
|
+
|
|
6
|
+
const hash=value=>createHash('sha256').update(value).digest('hex');
|
|
7
|
+
export async function executeCanvas(ctx,preset,args,exec){
|
|
8
|
+
const [{parseCanvasDocument},{appendCanvasBlock,updateCanvasBlock}]=await Promise.all([import('@oh-my-student/plugin-vault-sqlite/drawing-files'),import('@oh-my-student/plugin-vault-sqlite/canvas-flow')]);
|
|
9
|
+
const agent=exec.agent;
|
|
10
|
+
if(!agent||ctx.agentPresets.composedPreset(agent.ctx)!==preset)throw Error('需要当前课堂身份');
|
|
11
|
+
const workspace=await ctx.workspaceRegistry.resolveByPath(agent.session.header.cwd);
|
|
12
|
+
if(!workspace)throw Error('当前课堂未绑定工作区');
|
|
13
|
+
if(typeof args.path!=='string'||!args.path.endsWith('.canvas'))throw Error('请选择当前工作区内的 .canvas 文件');
|
|
14
|
+
if(!['read','append','update'].includes(args.action))throw Error('canvas_action_invalid');
|
|
15
|
+
exec.signal.throwIfAborted();
|
|
16
|
+
let record;
|
|
17
|
+
try{record=await ctx.notaraVault.read({workspaceId:workspace.id,path:args.path});}
|
|
18
|
+
catch(error){if(error.code!=='ENOENT'&&!String(error.message).includes('ENOENT'))throw error;record={content:'{"nodes":[],"edges":[]}',revision:null};}
|
|
19
|
+
let doc=parseCanvasDocument(record.content);
|
|
20
|
+
const summary=()=>({path:args.path,revision:record.revision,continuationAnchor:doc.nodes.some(n=>n.id===doc.notaraCanvas?.lastNodeId)?doc.notaraCanvas.lastNodeId:null,nodes:doc.nodes.map(n=>({id:n.id,title:(n.text?.split('\n')[0]??n.label??n.file??n.id).replace(/^#+\s*/,''),type:n.type,file:n.file,subpath:n.subpath,x:n.x,y:n.y,width:n.width,height:n.height,pinned:!n.notaraFlow||!!n.notaraFlow.pinned})),...(args.action==='read'?{}:{nodeId:args.action==='append'?id:args.nodeId})});
|
|
21
|
+
const id='board-'+hash(`${agent.id}:${exec.callId}`).slice(0,24),requestHash=hash(JSON.stringify(args));
|
|
22
|
+
if(args.action==='read')return summary();
|
|
23
|
+
const prior=args.action==='append'&&doc.nodes.find(n=>n.id===id);
|
|
24
|
+
if(prior){if(prior.notaraFlow?.requestHash!==requestHash)throw Error('canvas_request_conflict');return {...summary(),alreadyApplied:true};}
|
|
25
|
+
if(args.expectedRevision!==record.revision)throw Error('板书已更新或缺少版本;请先 canvas read,再基于最新锚点和 revision 操作。');
|
|
26
|
+
if(args.action==='append'){
|
|
27
|
+
if(doc.nodes.length&&!args.after)throw Error('已有板书必须指定 after:从 canvas read 的结果选择目标块或分组 ID。');
|
|
28
|
+
if(args.file!==undefined){const source=await ctx.notaraVault.read({workspaceId:workspace.id,path:args.file});if(args.mathElementId!==undefined)drawingMathElement(parseDrawingDocument(source.content),args.mathElementId);}
|
|
29
|
+
doc=appendCanvasBlock(doc,{id,title:args.title,body:args.body,file:args.file,mathElementId:args.mathElementId,after:args.after,placement:args.placement,size:args.size,operationId:exec.callId});
|
|
30
|
+
doc.nodes.find(n=>n.id===id).notaraFlow.requestHash=requestHash;
|
|
31
|
+
}else doc=updateCanvasBlock(doc,args);
|
|
32
|
+
exec.signal.throwIfAborted();
|
|
33
|
+
const result=await ctx.notaraVault.save({workspaceId:workspace.id,path:args.path,content:JSON.stringify(doc,null,2)+'\n',expectedRevision:record.revision});
|
|
34
|
+
record={...record,revision:result.revision};
|
|
35
|
+
const node=doc.nodes.find(n=>n.id===(args.action==='append'?id:args.nodeId));
|
|
36
|
+
const anchor=doc.nodes.find(n=>n.id===args.after);
|
|
37
|
+
return {...summary(),position:{x:node.x,y:node.y,width:node.width,height:node.height},...(anchor?{anchorId:anchor.id,placement:args.placement??'below',avoidedObstacles:node.y!==(anchor.y+(args.placement==='beside'?0:anchor.height+28))}:{}),note:'已保存;页面会同步。学生翻看旧内容时不会强制跳转。'};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function registerCanvasTool(ctx,preset){
|
|
41
|
+
ctx.tools.register(defineTool({
|
|
42
|
+
name:'canvas',description:'连续课堂板书:先 read 查看稳定节点 ID 与版本,再 append 新块或 update 原块。已有板书追加必须以 after 指定目标块/分组;below 接其底边,beside 接其右边。系统避让已有容器,保留学生位置。纠错用 update,禁止整文件重写。只负责板书,不代表学生掌握。',
|
|
43
|
+
parameters:{
|
|
44
|
+
action:{type:'string',enum:['read','append','update'],required:true},path:{type:'string',required:true,description:'当前工作区内的 .canvas 路径。'},
|
|
45
|
+
expectedRevision:{oneOf:[{type:'string'},{type:'null'}],description:'上次 read 返回的 revision;新建为 null。写入时必须提供。'},
|
|
46
|
+
title:{type:'string',description:'追加或修改的块标题。'},body:{type:'string',description:'Markdown 正文,可含 figure 和现有互动组件。'},
|
|
47
|
+
file:{type:'string',description:'append 可引用已存在的工作区资料文件,例如 .excalidraw;与 body 二选一,生成真实 file 节点。'},
|
|
48
|
+
mathElementId:{type:'string',description:'append引用数学对象时可用:先excalidraw read,选择具有math字段的真实元素ID,配合file引用该.excalidraw中的单张可交互数学图。共享同一源对象,不能编造ID。省略则展示整个文件。'},
|
|
49
|
+
after:{type:'string',description:'append 的锚点 ID,必须来自 read 结果;空画布首块省略。'},
|
|
50
|
+
placement:{type:'string',enum:['below','beside'],description:'相对 after 的方位,默认 below。'},size:{type:'string',enum:['normal','wide']},
|
|
51
|
+
nodeId:{type:'string',description:'update 的目标 ID,来自 read;保留该块身份、位置和其他字段。'},
|
|
52
|
+
},
|
|
53
|
+
output:{schema:{type:'object',additionalProperties:false,properties:{result:{type:'string',required:true}}},render:(_args,value)=>[{type:'text',text:value.result}]},
|
|
54
|
+
async execute(args,exec){return {result:JSON.stringify(await executeCanvas(ctx,preset,args,exec))};},
|
|
55
|
+
}));
|
|
56
|
+
}
|
package/catalog.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { resource } from './resources.js';
|
|
2
|
+
import { BLIND_PRESET_ID, BLIND_SYSTEM_PROMPT } from './isolation.js';
|
|
3
|
+
|
|
4
|
+
export const SKILL_NAMES = Object.freeze([
|
|
5
|
+
'notara-quest-orchestrator', 'notara-pdf-learning-set', 'notara-quest-tutor',
|
|
6
|
+
'notara-examiner-analysis', 'notara-review-brainstorm',
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
const roles = [
|
|
10
|
+
{ id: 'notara-coordinator', name: '首页学习助手', description: '倾听、查阅、规划,并将具体学习交接给专题课堂。', kind: 'coordinator', prompt: resource('coordinator.md') },
|
|
11
|
+
{ id: 'notara-teacher', name: '专题课堂老师', description: '围绕真实材料引导研讨、练习和整理。', kind: 'teacher', prompt: resource('base.md') },
|
|
12
|
+
...[
|
|
13
|
+
['deepseek', '大肥鱼', '费曼听讲与橡皮鸭调试。'],
|
|
14
|
+
['claude', '大小姐', '梳理结构,寻找简洁路径。'],
|
|
15
|
+
['gemini', '多动鬼', '图像直觉与跨领域类比。'],
|
|
16
|
+
['gpt', '后桌', '检查前提、边界与反例。'],
|
|
17
|
+
].map(([id, name, description]) => ({ id: `companion-${id}`, name, description, kind: 'companion', prompt: resource(`companions/${id}.md`) })),
|
|
18
|
+
{ id: BLIND_PRESET_ID, name: '大肥鱼(不看解析)', description: '只接收原始题面与学生本次讲解,禁用工具与父会话历史。', kind: 'blind', prompt: BLIND_SYSTEM_PROMPT },
|
|
19
|
+
...[
|
|
20
|
+
['problem', '题目研究员', '独立推演与 Python/SymPy 验算。'],
|
|
21
|
+
['review', '核验员', '检查逻辑、依据和遗漏条件。'],
|
|
22
|
+
['exercise', '出题员', '设计并核验变式与铺垫题。'],
|
|
23
|
+
['general', '资料研究员', '检索本地材料与公开来源。'],
|
|
24
|
+
['lesson', '备课员', '梳理学习路径与备课草案。'],
|
|
25
|
+
].map(([id, name, description]) => ({ id: `worker-${id}`, name, description, kind: 'worker', prompt: `${resource('workers/base.md')}\n\n${resource(`workers/${id}.md`)}` })),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const ROLES = Object.freeze(roles.map(Object.freeze));
|
|
29
|
+
export const PRESET_IDS = Object.freeze(ROLES.map(role => role.id));
|
|
30
|
+
export function roleById(id) {
|
|
31
|
+
const role = ROLES.find(role => role.id === id);
|
|
32
|
+
if (!role) throw new TypeError(`Unknown Notara preset: ${id}`);
|
|
33
|
+
return role;
|
|
34
|
+
}
|
package/drawing-tool.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {defineTool} from '@deepseek-ai/dsh-tools';
|
|
3
|
+
import {createLibraryDrawing,searchDrawingLibrary} from '@oh-my-student/plugin-vault-sqlite/drawing-library';
|
|
4
|
+
import {PLANAR_CONSTRUCTION_SYNTAX} from '@oh-my-student/plugin-graph-and-blocks/drawing-math';
|
|
5
|
+
const hash=text=>createHash('sha256').update(text).digest('hex');
|
|
6
|
+
export async function executeDrawing(ctx,preset,args,exec){
|
|
7
|
+
const [{parseDrawingDocument,EMPTY_DRAWING},{makeDrawingElements,drawingBounds,validateDrawing}]=await Promise.all([import('@oh-my-student/plugin-vault-sqlite/drawing-files'),import('@oh-my-student/plugin-vault-sqlite/drawing-sync')]);
|
|
8
|
+
const agent=exec.agent;if(!agent||ctx.agentPresets.composedPreset(agent.ctx)!==preset)throw Error('需要当前课堂身份');
|
|
9
|
+
const workspace=await ctx.workspaceRegistry.resolveByPath(agent.session.header.cwd);if(!workspace)throw Error('当前课堂未绑定工作区');
|
|
10
|
+
if(args.action==='library')return {result:JSON.stringify({source:'ExcaliMath',items:searchDrawingLibrary(args.query).map(({id,name,english,category})=>({id,name,english,category})),note:'现成示意素材;draw 使用 kind=library、libraryId 和局部 x/y。不是约束几何或三维构造。'})};
|
|
11
|
+
if(typeof args.path!=='string'||!args.path.endsWith('.excalidraw'))throw Error('drawing_path_invalid: 请指定 .excalidraw 文件');
|
|
12
|
+
if(!['read','draw','update','delete','show'].includes(args.action))throw Error('drawing_action_invalid');
|
|
13
|
+
exec.signal.throwIfAborted();let record;
|
|
14
|
+
try{record=await ctx.notaraVault.read({workspaceId:workspace.id,path:args.path});}catch(e){if(e.code!=='ENOENT'&&!String(e.message).includes('ENOENT'))throw e;record={content:EMPTY_DRAWING,revision:null};}
|
|
15
|
+
let doc=parseDrawingDocument(record.content),elements=doc.elements.filter(e=>!e.isDeleted);
|
|
16
|
+
const presentation=(content,revision)=>{
|
|
17
|
+
const scene=parseDrawingDocument(content);delete scene.notaraDrawing;
|
|
18
|
+
const snapshot=JSON.stringify(scene);if(snapshot.length>4*1024*1024)return {};
|
|
19
|
+
return {snapshot:{version:1,sourceSessionId:agent.id,workspaceId:workspace.id,path:args.path,revision,content:snapshot}};
|
|
20
|
+
};
|
|
21
|
+
const embed=()=>`\`\`\`drawing\n${exec.callId}\n\`\`\``;
|
|
22
|
+
if(args.action==='show'){
|
|
23
|
+
const shown=presentation(record.content,record.revision);if(!shown.snapshot)throw Error('drawing_snapshot_too_large: 请拆分白板后展示');
|
|
24
|
+
return {result:JSON.stringify({path:args.path,revision:record.revision,embed:embed(),note:'在回复正文中原样放入 embed,即展示这一版本的白板。'}),...shown};
|
|
25
|
+
}
|
|
26
|
+
const groups=[...new Set(elements.flatMap(e=>e.groupIds??[]))].map(id=>({id,title:elements.find(e=>e.groupIds?.includes(id))?.customData?.notaraTitle??'图形组',...drawingBounds(elements.filter(e=>e.groupIds?.includes(id)))}));
|
|
27
|
+
if(args.action==='read'){
|
|
28
|
+
const preview=await ctx.notaraVault.drawingPreview({workspaceId:workspace.id,path:args.path});
|
|
29
|
+
const selected=args.elementIds?.length?elements.filter(e=>args.elementIds.includes(e.id)||e.groupIds?.some(id=>args.elementIds.includes(id))):elements;
|
|
30
|
+
const result={path:args.path,revision:record.revision,groups,busyIds:preview.busyIds,elements:selected.slice(0,300).map(e=>({id:e.id,type:e.type,text:e.text,x:e.x,y:e.y,width:e.width,height:e.height,angle:e.angle,groupIds:e.groupIds,version:e.version,...(e.customData?.notaraMath?{math:e.customData.notaraMath}:{})})),truncated:selected.length>300};
|
|
31
|
+
let image;
|
|
32
|
+
if(preview.png&&preview.revision===record.revision&&ctx.get('attachments')){
|
|
33
|
+
const routed=agent.session.requestHeader?.()?.config??agent.options;
|
|
34
|
+
const model=routed?.provider&&routed?.model&&await ctx.get('llm')?.resolveModelInfo(routed.provider,routed.model,exec.signal);
|
|
35
|
+
if(model?.inputModalities?.includes('image'))image=await ctx.get('attachments').saveImage({data:Buffer.from(preview.png,'base64'),mediaType:'image/png',name:'当前画布.png'});
|
|
36
|
+
else result.preview='当前模型不支持图片;手写内容不能只凭元素坐标推断,请切换支持图片的模型。';
|
|
37
|
+
}else result.preview='当前版本的图片预览尚未就绪;打开画布后会生成。不能声称看过手写内容。';
|
|
38
|
+
return {result:JSON.stringify(result),...(image?{image}: {})};
|
|
39
|
+
}
|
|
40
|
+
const operationId=hash(`${agent.id}:${exec.callId}`),requestHash=hash(JSON.stringify(args));
|
|
41
|
+
if(doc.notaraDrawing?.receipts?.[operationId]){if(doc.notaraDrawing.receipts[operationId]!==requestHash)throw Error('drawing_request_conflict');return {result:JSON.stringify({path:args.path,revision:record.revision,alreadyApplied:true})};}
|
|
42
|
+
if(args.expectedRevision!==record.revision)throw Error('drawing_revision_conflict: 请先 read,使用最新 revision 和目标 ID。');
|
|
43
|
+
const before=record.content;doc=structuredClone(doc);let affected=[];
|
|
44
|
+
if(args.action==='draw'){
|
|
45
|
+
if(!Array.isArray(args.shapes)||!args.shapes.length||args.shapes.length>100)throw Error('drawing_shapes_invalid: 需要1–100个图形');
|
|
46
|
+
const expanded=[];
|
|
47
|
+
for(const shape of args.shapes){
|
|
48
|
+
if(shape.kind==='math'){const {mathDefinition}=await import('@oh-my-student/plugin-graph-and-blocks/drawing-math');expanded.push({...shape,math:mathDefinition(shape.source).data});continue;}
|
|
49
|
+
if(shape.kind!=='function'){expanded.push(shape);continue;}
|
|
50
|
+
const {compileExpression}=await import('@oh-my-student/plugin-graph-and-blocks/math-expression');
|
|
51
|
+
const [xmin,xmax]=shape.xRange??[],[ymin,ymax]=shape.yRange??[];
|
|
52
|
+
if(![xmin,xmax,ymin,ymax,shape.x,shape.y,shape.width,shape.height].every(Number.isFinite)||xmin>=xmax||ymin>=ymax||shape.width<=0||shape.height<=0)throw Error('drawing_function_range_invalid');
|
|
53
|
+
const f=compileExpression(shape.expression,['x']);let points=[];
|
|
54
|
+
const flush=()=>{if(points.length>=2)expanded.push({kind:'line',x:shape.x,y:shape.y,points,color:shape.color});points=[];};
|
|
55
|
+
for(let i=0;i<=160;i++){const x=xmin+(xmax-xmin)*i/160;let y;try{y=f.evaluate({x});}catch{y=NaN;}
|
|
56
|
+
if(!Number.isFinite(y)||y<ymin||y>ymax){flush();continue;}
|
|
57
|
+
const point=[(x-xmin)/(xmax-xmin)*shape.width,(ymax-y)/(ymax-ymin)*shape.height];
|
|
58
|
+
if(points.length&&Math.abs(point[1]-points.at(-1)[1])>shape.height/2)flush();points.push(point);
|
|
59
|
+
}flush();
|
|
60
|
+
}
|
|
61
|
+
let serial=0;const nextId=()=>`${operationId.slice(0,22)}-${serial++}`;
|
|
62
|
+
const created=expanded.flatMap(shape=>{
|
|
63
|
+
if(shape.kind!=='library')return makeDrawingElements([shape],nextId);
|
|
64
|
+
if(!Number.isFinite(shape.x)||!Number.isFinite(shape.y))throw Error('drawing_position_invalid');
|
|
65
|
+
const inserted=createLibraryDrawing(shape.libraryId,nextId);Object.assign(doc.files??={},inserted.files);
|
|
66
|
+
return inserted.elements.map(e=>({...e,x:e.x+shape.x,y:e.y+shape.y}));
|
|
67
|
+
}),groupId=`group-${operationId.slice(0,22)}`;
|
|
68
|
+
const placement=args.placement??'below';if(!['below','beside','overlay'].includes(placement))throw Error('drawing_placement_invalid');
|
|
69
|
+
const target=elements.find(e=>e.id===args.after),anchor=target?drawingBounds([target]):groups.find(g=>g.id===args.after);
|
|
70
|
+
if(elements.length&&!anchor)throw Error('drawing_anchor_missing: 非空画布需要 after 指定 read 返回的元素或组 ID');
|
|
71
|
+
const bounds=drawingBounds(created);let x=anchor?anchor.x+(placement==='beside'?anchor.width+32:0):0,y=anchor?anchor.y+(placement==='below'?anchor.height+32:0):0;
|
|
72
|
+
if(placement!=='overlay'){
|
|
73
|
+
const obstacles=[...groups,...elements.filter(e=>!e.groupIds?.length)].map(e=>drawingBounds(e.type?[e]:[{...e,angle:0}]));let obstacle;
|
|
74
|
+
while((obstacle=obstacles.find(o=>x<o.x+o.width+32&&x+bounds.width+32>o.x&&y<o.y+o.height+32&&y+bounds.height+32>o.y)))y=obstacle.y+obstacle.height+32;
|
|
75
|
+
}
|
|
76
|
+
for(const e of created){e.x+=x-bounds.x;e.y+=y-bounds.y;e.groupIds=[groupId];e.customData={...e.customData,notaraAuthor:'teacher',notaraTitle:String(args.title??'板书').slice(0,160)};}
|
|
77
|
+
doc.elements.push(...created);affected=created.map(e=>e.id);
|
|
78
|
+
}else{
|
|
79
|
+
const updates=args.action==='delete'?(args.elementIds??[]).map(id=>({id,isDeleted:true})):args.updates;
|
|
80
|
+
if(!Array.isArray(updates)||!updates.length||updates.length>100)throw Error('drawing_updates_invalid');
|
|
81
|
+
for(const patch of updates){
|
|
82
|
+
const e=doc.elements.find(e=>e.id===patch.id&&!e.isDeleted);if(!e)throw Error('drawing_element_missing: 目标已删除或不存在,请重新读取');
|
|
83
|
+
const allowed=['id','x','y','width','height','text','color','source',...(args.action==='delete'?['isDeleted']:[])];if(Object.keys(patch).some(k=>!allowed.includes(k)))throw Error('drawing_patch_invalid');
|
|
84
|
+
const {id,color,source,...changes}=patch;if(changes.text!==undefined){if(e.type!=='text'||typeof changes.text!=='string'||changes.text.length>8000)throw Error('drawing_text_invalid');changes.originalText=changes.text;changes.height=changes.text.split('\n').length*e.fontSize*e.lineHeight;}
|
|
85
|
+
if(source!==undefined){if(!e.customData?.notaraMath)throw Error('drawing_math_target_invalid');const {mathDefinition}=await import('@oh-my-student/plugin-graph-and-blocks/drawing-math');changes.customData={...e.customData,notaraMath:mathDefinition(source).data};}
|
|
86
|
+
if(color!==undefined){if(!/^#[\da-f]{6}$/i.test(color))throw Error('drawing_color_invalid');changes.strokeColor=color;}
|
|
87
|
+
Object.assign(e,changes,{version:(e.version??1)+1,versionNonce:Math.floor(Math.random()*2147483647),updated:Date.now()});affected.push(id);
|
|
88
|
+
}
|
|
89
|
+
if(args.action==='delete')for(const e of doc.elements){
|
|
90
|
+
if(e.isDeleted)continue;let changed=false;
|
|
91
|
+
for(const key of ['startBinding','endBinding'])if(affected.includes(e[key]?.elementId)){e[key]=null;changed=true;}
|
|
92
|
+
for(const key of ['containerId','frameId'])if(affected.includes(e[key])){e[key]=null;changed=true;}
|
|
93
|
+
if(e.boundElements?.some(b=>affected.includes(b.id))){e.boundElements=e.boundElements.filter(b=>!affected.includes(b.id));changed=true;}
|
|
94
|
+
if(changed){e.version=(e.version??1)+1;e.versionNonce=Math.floor(Math.random()*2147483647);e.updated=Date.now();}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
validateDrawing(doc);exec.signal.throwIfAborted();
|
|
98
|
+
const result=await ctx.notaraVault.drawingCommit({workspaceId:workspace.id,path:args.path,base:before,content:JSON.stringify(doc),respectActivity:true,operationId,requestHash});
|
|
99
|
+
const shown=presentation(result.content,result.revision);
|
|
100
|
+
return {result:JSON.stringify({path:args.path,revision:result.revision,elementIds:affected,...(shown.snapshot?{embed:embed()}:{}),note:'图形已保存。需要在回复中展示这一版本时,原样放入 embed;不会自动向学生发送消息。'}),...shown};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function registerDrawingTool(ctx,preset){ctx.tools.register(defineTool({
|
|
104
|
+
name:'excalidraw',description:'在当前课堂共绘画布读取、追加、修改或删除图形。现成图形先用library按query搜索,再以kind=library和libraryId插入。先read取得revision和目标元素/组ID。draw提交局部坐标图形组,已有画布必须指定after;below/beside避让,overlay用于在目标上批注。update/delete遇到学生正在编辑或版本变化会拒绝,需等待后重读。禁止整文件重写;新增批注保留学生原笔迹。图片预览仅在当前模型支持图片时提供。',
|
|
105
|
+
parameters:{action:{type:'string',enum:['read','draw','update','delete','show','library'],required:true,description:'show只冻结当前白板并返回可放入回复的embed,不修改图形。'},query:{type:'string',description:'library的中英文搜索词,空值返回全部素材。'},path:{type:'string',description:'除library外必填:当前工作区内的 .excalidraw 相对路径。'},expectedRevision:{oneOf:[{type:'string'},{type:'null'}],description:'写操作必须提供read返回的revision;空白新文件为null。'},after:{type:'string',description:'read返回的元素或组ID;非空画布draw必须提供。'},placement:{type:'string',enum:['below','beside','overlay']},title:{type:'string',description:'本次图形组的简短标题。'},elementIds:{type:'array',items:{type:'string'},description:'read筛选或delete指定的真实元素ID。'},
|
|
106
|
+
shapes:{type:'array',description:'draw的图形。坐标是本组内部坐标,由系统整体放到锚点旁。数学图优先使用math和source,保留定义与交互;function仅生成静态曲线。function按数学表达式采样,必须给xRange/yRange和显示宽高;text使用普通文字或Unicode数学符号。',items:{type:'object',additionalProperties:false,properties:{kind:{type:'string',enum:['rectangle','ellipse','diamond','text','line','arrow','function','math','library'],required:true},libraryId:{type:'string',description:'kind=library必填:library查询返回的素材ID。'},x:{type:'number',required:true},y:{type:'number',required:true},width:{type:'number'},height:{type:'number'},text:{type:'string'},fontSize:{type:'number'},color:{type:'string',description:'#RRGGBB'},points:{type:'array',items:{type:'array',items:{type:'number'}},description:'line/arrow的局部点坐标,例如[[0,0],[100,50]]。'},source:{type:'string',description:'math图的figure定义(不含围栏),一行一个声明。最小例:axes x -2..2 y -1..5\nparam a = 1 in 0.5..2 step 0.1\nfunction f(x) = a*x^2。支持现有figure的平面和基础空间语法,不允许ask或JavaScript。\n'+PLANAR_CONSTRUCTION_SYNTAX},expression:{type:'string',description:'function使用现有数学语法,如x^2、sin(x)、ln(x),只允许变量x。'},xRange:{type:'array',items:{type:'number'}},yRange:{type:'array',items:{type:'number'}}}}},
|
|
107
|
+
updates:{type:'array',description:'update按id修改指定字段,未给字段保留。',items:{type:'object',additionalProperties:false,properties:{id:{type:'string',required:true},x:{type:'number'},y:{type:'number'},width:{type:'number'},height:{type:'number'},text:{type:'string'},color:{type:'string'},source:{type:'string',description:'仅用于数学图:替换完整figure定义,并重置该图的参数、自由点和视角。'}}}},
|
|
108
|
+
},output:{schema:{type:'object',additionalProperties:false,properties:{result:{type:'string',required:true},image:{type:'json'},snapshot:{type:'json'}}},presentationMeta:(_args,value)=>value.snapshot?{notaraDrawing:value.snapshot}:{},render:(_args,value)=>[{type:'text',text:value.result},...(value.image?[{type:'image',attachment:value.image}]:[])]},execute:(args,exec)=>executeDrawing(ctx,preset,args,exec),
|
|
109
|
+
}));}
|
package/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
3
|
+
import { ROLES } from './catalog.js';
|
|
4
|
+
import { skillsDirectory } from './resources.js';
|
|
5
|
+
import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent';
|
|
6
|
+
import { TopicConversations } from './topic-conversations.js';
|
|
7
|
+
|
|
8
|
+
export { PRESET_IDS, ROLES, SKILL_NAMES } from './catalog.js';
|
|
9
|
+
export { BLIND_PRESET_ID } from './isolation.js';
|
|
10
|
+
|
|
11
|
+
export function presetDefinitions() {
|
|
12
|
+
return ROLES.map((role, order) => ({
|
|
13
|
+
id: role.id, name: role.name, description: role.description, order,
|
|
14
|
+
plugins: [
|
|
15
|
+
{ id: 'notara-prompt', name: '@oh-my-student/plugin-presets/prompt', config: { preset: role.id } },
|
|
16
|
+
{ id: 'notara-policy', name: '@oh-my-student/plugin-presets/policy', config: { preset: role.id } },
|
|
17
|
+
...['teacher', 'coordinator'].includes(role.kind) ? [
|
|
18
|
+
{ id: 'tool-ask-user', name: '@deepseek-ai/dsh-tool-ask-user', config: { mode: 'timed', timeout: -1 } },
|
|
19
|
+
] : [],
|
|
20
|
+
...role.kind === 'blind' ? [] : [
|
|
21
|
+
{ id: 'tool-fs', name: '@deepseek-ai/dsh-tool-fs' },
|
|
22
|
+
{ id: 'tool-fs-search', name: '@deepseek-ai/dsh-tool-fs-search', config: { sampleOverCapGlobResults: false } },
|
|
23
|
+
],
|
|
24
|
+
...['teacher', 'worker', 'coordinator'].includes(role.kind) ? [{ id: 'tool-web', name: '@deepseek-ai/dsh-tool-web', config: { fetch: role.kind !== 'coordinator' } }] : [],
|
|
25
|
+
...role.kind === 'teacher' || ['worker-problem', 'worker-exercise'].includes(role.id) ? [{ id: 'tool-bash', name: '@deepseek-ai/dsh-tool-bash' }] : [],
|
|
26
|
+
...['teacher', 'worker'].includes(role.kind) ? [
|
|
27
|
+
{ id: 'notara-skills', name: '@deepseek-ai/dsh-skill-filesystem', config: { providerName: 'notara-bundled', includeDefaultRoots: false, bundledSkillDir: fileURLToPath(skillsDirectory), watch: false } },
|
|
28
|
+
{ id: 'tool-skill', name: '@deepseek-ai/dsh-tool-skill' },
|
|
29
|
+
] : [],
|
|
30
|
+
...['teacher', 'coordinator', 'companion'].includes(role.kind) ? [
|
|
31
|
+
{ id: 'notara-delegation', name: '@oh-my-student/plugin-presets/tools', config: { preset: role.id } },
|
|
32
|
+
] : [],
|
|
33
|
+
...role.kind === 'teacher' ? [{ id: 'tool-jobs', name: '@deepseek-ai/dsh-tool-jobs' }] : [],
|
|
34
|
+
],
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Same registration API as dsh-agent-preset's Service.init, with disposers
|
|
39
|
+
// retained by Cordis. No host services are published from a preset group.
|
|
40
|
+
export default class NotaraPresets {
|
|
41
|
+
static inject = ['agentPresets'];
|
|
42
|
+
constructor(ctx) { this.ctx = ctx; }
|
|
43
|
+
async *[Service.init]() {
|
|
44
|
+
const ctx = this.ctx;
|
|
45
|
+
ctx.inject(TopicConversations.inject, runtimeCtx => { runtimeCtx.plugin(TopicConversations); });
|
|
46
|
+
// Preset registration has exactly the native preset plugin's dependency.
|
|
47
|
+
// Host services may arrive independently; model-facing prompt/tool services
|
|
48
|
+
// need not exist in this root plugin's dependency scope.
|
|
49
|
+
ctx.inject(['agents', 'subagents'], runtimeCtx => {
|
|
50
|
+
// Native continuations initially inherit their parent's preset. Commit the
|
|
51
|
+
// teacher selection before publication releases the first queued prompt.
|
|
52
|
+
// The native selection event also restores the right role on cold resume.
|
|
53
|
+
runtimeCtx.on('agent/created', async ({ agent }) => {
|
|
54
|
+
const events = agent.session.snapshotEvents();
|
|
55
|
+
const descriptor = foldSubagentDescriptor(events);
|
|
56
|
+
if (descriptor?.mode !== 'continuable' || descriptor.provider !== 'notara-preset:notara-teacher') return;
|
|
57
|
+
if (runtimeCtx.agentPresets.composedPreset(agent.ctx) !== 'notara-teacher') {
|
|
58
|
+
const recorded = events.filter(e => e.type === 'agent-preset/selected').at(-1)?.data.agentPreset ?? agent.session.header.agentPreset;
|
|
59
|
+
// Restoring the recorded composition is not a new selection in an
|
|
60
|
+
// already-started classroom. No prompt can run until this listener ends.
|
|
61
|
+
if (recorded === 'notara-teacher') await runtimeCtx.agentPresets.mount(agent.ctx, recorded);
|
|
62
|
+
else await runtimeCtx.agentPresets.select(agent, 'notara-teacher');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
for (const definition of presetDefinitions()) yield await ctx.agentPresets.register(definition);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/isolation.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const BLIND_PRESET_ID = 'companion-deepseek-blind';
|
|
2
|
+
export const BLIND_SYSTEM_PROMPT = '你是认真听讲的大肥鱼。你只能看到原始题面和学生本次讲解,不知道标准答案。请定位第一个缺少前提、逻辑跳步或边界不明之处,一次追问一个具体问题;无法确认时明确说不知道,不假装已验证。';
|
|
3
|
+
|
|
4
|
+
function text(value, field, max = 48_000) {
|
|
5
|
+
if (typeof value !== 'string' || !value.trim() || value.length > max) {
|
|
6
|
+
throw new TypeError(`${field} must be non-empty text of at most ${max} characters`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Deliberately accepts no generic prompt/context/history/reference fields. */
|
|
12
|
+
export function blindTask(input) {
|
|
13
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('Blind task must be an object');
|
|
14
|
+
const allowed = new Set(['problem', 'studentExplanation']);
|
|
15
|
+
for (const key of Object.keys(input)) {
|
|
16
|
+
if (!allowed.has(key)) throw new TypeError(`Blind task does not accept ${key}`);
|
|
17
|
+
}
|
|
18
|
+
return `## 原始题面\n${text(input.problem, 'problem')}\n\n## 学生本次讲解\n${text(input.studentExplanation, 'studentExplanation')}`;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-student/plugin-presets",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Native DSH teaching presets and bundled filesystem skills",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=24"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./index.js",
|
|
12
|
+
"./prompt": "./prompt.js",
|
|
13
|
+
"./policy": "./policy.js",
|
|
14
|
+
"./tools": "./tools.js",
|
|
15
|
+
"./catalog": "./catalog.js",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"*.js",
|
|
20
|
+
"resources",
|
|
21
|
+
"COPYRIGHT",
|
|
22
|
+
"THIRD_PARTY_NOTICES.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test test/*.test.js",
|
|
26
|
+
"sync:resources": "node scripts/sync-resources.mjs"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@oh-my-student/plugin-vault-sqlite": "0.1.1",
|
|
30
|
+
"@oh-my-student/plugin-graph-and-blocks": "0.1.1",
|
|
31
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
32
|
+
"@deepseek-ai/dsh-agent": "0.2.1-rc.4",
|
|
33
|
+
"@deepseek-ai/dsh-agent-preset": "0.2.1-rc.4",
|
|
34
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.4",
|
|
35
|
+
"@deepseek-ai/dsh-scope": "0.2.1-rc.4",
|
|
36
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.4",
|
|
37
|
+
"@deepseek-ai/dsh-session-query": "0.2.1-rc.4",
|
|
38
|
+
"@deepseek-ai/dsh-skill-filesystem": "0.2.1-rc.4",
|
|
39
|
+
"@deepseek-ai/dsh-subagent": "0.2.1-rc.4",
|
|
40
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.1-rc.4",
|
|
41
|
+
"@deepseek-ai/dsh-tool-bash": "0.2.1-rc.4",
|
|
42
|
+
"@deepseek-ai/dsh-tool-ask-user": "0.2.1-rc.4",
|
|
43
|
+
"@deepseek-ai/dsh-user-questions": "0.2.1-rc.4",
|
|
44
|
+
"@deepseek-ai/dsh-tool-fs": "0.2.1-rc.4",
|
|
45
|
+
"@deepseek-ai/dsh-tool-fs-search": "0.2.1-rc.4",
|
|
46
|
+
"@deepseek-ai/dsh-tool-skill": "0.2.1-rc.4",
|
|
47
|
+
"@deepseek-ai/dsh-tool-jobs": "0.2.1-rc.4",
|
|
48
|
+
"@deepseek-ai/dsh-tool-web": "0.2.1-rc.4",
|
|
49
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.4"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@oh-my-student/plugin-vault-sqlite": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@oh-my-student/plugin-graph-and-blocks": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": "git+https://github.com/TongZi2003/Notara-Desktop.git",
|
|
62
|
+
"directory": "notara-core/plugins/presets"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public",
|
|
66
|
+
"registry": "https://registry.npmjs.org/"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/policy.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ROLES, roleById } from './catalog.js';
|
|
2
|
+
import { scopeParentOf, scopeOf, scopeChainOf } from '@deepseek-ai/dsh-scope';
|
|
3
|
+
|
|
4
|
+
const read = ['read', 'read_image', 'glob', 'grep'];
|
|
5
|
+
export function allowedTools(role) {
|
|
6
|
+
switch (role.kind) {
|
|
7
|
+
case 'blind': return [];
|
|
8
|
+
case 'coordinator': return ['read', 'glob', 'grep', 'web_search', 'subagent', 'send_message', 'ask_user_question'];
|
|
9
|
+
case 'teacher': return [...read, 'write', 'edit', 'bash', 'web_search', 'web_fetch', 'skill', 'subagent', 'send_message', 'interrupt_agent', 'job_output', 'job_list', 'job_kill', 'ask_user_question', 'canvas','excalidraw'];
|
|
10
|
+
case 'companion': return [...read, 'send_message'];
|
|
11
|
+
case 'worker': return [...read, 'web_search', 'web_fetch', 'skill', ...['worker-problem', 'worker-exercise'].includes(role.id) ? ['bash'] : []];
|
|
12
|
+
default: throw new TypeError('Unknown role kind');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// A retained preset generation can outlive the plugin that registered it.
|
|
17
|
+
// The standing policy row admits Agents; each admitted Agent owns its own
|
|
18
|
+
// controller, listeners, restriction and guard until that Agent is disposed.
|
|
19
|
+
const controllers = new WeakMap();
|
|
20
|
+
|
|
21
|
+
// Match the native userQuestions ownership boundary, including restored roots
|
|
22
|
+
// that still have a durable parent-session lineage.
|
|
23
|
+
const toolsForAgent = (ctx, agent, role) => allowedTools(role).filter(name =>
|
|
24
|
+
name !== 'ask_user_question' || ctx.agents.roots().includes(agent));
|
|
25
|
+
|
|
26
|
+
function attachPolicy(agent) {
|
|
27
|
+
const existing = controllers.get(agent);
|
|
28
|
+
if (existing) { existing.reconcile(); return; }
|
|
29
|
+
const ctx = agent.ctx;
|
|
30
|
+
const registry = ctx.get('agentPresets');
|
|
31
|
+
let installed;
|
|
32
|
+
let reconciling = false;
|
|
33
|
+
let disposed = false;
|
|
34
|
+
function reconcile() {
|
|
35
|
+
if (reconciling || disposed) return;
|
|
36
|
+
reconciling = true;
|
|
37
|
+
try {
|
|
38
|
+
const id = registry.composedPreset(ctx);
|
|
39
|
+
const role = ROLES.find(role => role.id === id);
|
|
40
|
+
const names = role ? toolsForAgent(ctx, agent, role) : [];
|
|
41
|
+
const available = names.filter(name => ctx.tools.get(name, scopeParentOf(agent)) !== undefined);
|
|
42
|
+
const signature = available.join(',');
|
|
43
|
+
if (installed?.id === id && installed.signature === signature) return;
|
|
44
|
+
installed?.dispose();
|
|
45
|
+
installed = undefined;
|
|
46
|
+
if (!role) return;
|
|
47
|
+
const allowed = new Set(names);
|
|
48
|
+
const disposers = [];
|
|
49
|
+
const state = { id, signature, dispose: () => disposers.splice(0).reverse().forEach(dispose => dispose()) };
|
|
50
|
+
installed = state;
|
|
51
|
+
try {
|
|
52
|
+
disposers.push(ctx.tools.presentAs('native'));
|
|
53
|
+
disposers.push(ctx.tools.restrict({ allow: available }));
|
|
54
|
+
disposers.push(ctx.tools.guard(exec => allowed.has(exec.name) ? undefined : `Tool ${exec.name} is unavailable for ${id}`));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
installed = undefined;
|
|
57
|
+
state.dispose();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
} finally { reconciling = false; }
|
|
61
|
+
}
|
|
62
|
+
controllers.set(agent, { reconcile });
|
|
63
|
+
ctx.on('tools/change', reconcile);
|
|
64
|
+
// rc.4 may inject a canonical-directory notice while creating a child.
|
|
65
|
+
// Blind assessment has no filesystem tools and admits no environment hints.
|
|
66
|
+
// Filter at the native admission boundary so model input remains replayable.
|
|
67
|
+
ctx.on('agent/pre-step', async (_payload, next) => {
|
|
68
|
+
const decision=await next();
|
|
69
|
+
if(decision.kind!=='enter')return decision;
|
|
70
|
+
const events=agent.session.snapshotEvents();
|
|
71
|
+
const initialChild=events.some(event=>event.type==='subagent/descriptor')&&!events.some(event=>event.type==='user/message');
|
|
72
|
+
const blind=registry.composedPreset(ctx)==='companion-deepseek-blind';
|
|
73
|
+
return {...decision,messages:decision.messages.filter(message=>!blind||message.source?.kind!=='working-directory').map(message=>initialChild&&message.source?.kind==='user'&&!message.source.rpcId?{...message,source:{kind:'notara-child-handoff',form:'relay'}}:message)};
|
|
74
|
+
});
|
|
75
|
+
// The listener is Agent-owned, as is the execution guard. Neither is revoked
|
|
76
|
+
// when Subagent providers or the registering presets root disappear.
|
|
77
|
+
ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
78
|
+
const result = await next();
|
|
79
|
+
if (context.scope !== agent) return result;
|
|
80
|
+
const role = ROLES.find(role => role.id === registry.composedPreset(ctx));
|
|
81
|
+
if (!role) return result;
|
|
82
|
+
const allowed = new Set(toolsForAgent(ctx, agent, role));
|
|
83
|
+
return { ...result, tools: result.tools.filter(tool => allowed.has(tool.name)) };
|
|
84
|
+
});
|
|
85
|
+
ctx.effect(() => () => { disposed = true; installed?.dispose(); controllers.delete(agent); });
|
|
86
|
+
reconcile();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const name = 'notara-agent-policy';
|
|
90
|
+
export const inject = ['agents'];
|
|
91
|
+
export function apply(ctx, { preset }) {
|
|
92
|
+
roleById(preset);
|
|
93
|
+
const generation = scopeOf(ctx);
|
|
94
|
+
if (!generation) throw new Error('Notara permissions require a standing preset scope');
|
|
95
|
+
const admit = agent => { if (scopeChainOf(agent).includes(generation)) attachPolicy(agent); };
|
|
96
|
+
ctx.on('agent/created', ({ agent }) => admit(agent));
|
|
97
|
+
for (const agent of ctx.agents.list()) admit(agent);
|
|
98
|
+
}
|
package/prompt.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { roleById } from './catalog.js';
|
|
2
|
+
import { readReviewContext } from './review-context.js';
|
|
3
|
+
|
|
4
|
+
export const name = 'notara-preset-prompt';
|
|
5
|
+
export const inject = ['systemPrompt'];
|
|
6
|
+
|
|
7
|
+
function runtimeContract(role) {
|
|
8
|
+
if (role.kind === 'blind') return '';
|
|
9
|
+
const lines = ['\n\n## 当前 DSH 工具合同', '文件工具使用实际公布的 schema:文件路径参数名为 file_path。学生从阅读器带入 PDF 时,页码/选区通过原生图片附件提供,并保留来源定位与版本;只根据实际收到的图片说明该页或选区内容。当前原生 read 不支持 PDF page/rect 图像读取参数;只有定位文字、没有图片时,不得声称已查看原页。'];
|
|
10
|
+
if (['teacher', 'worker'].includes(role.kind)) lines.push('正文中的 read({path:"skill:名称"}) 改用原生 skill({name:"名称"}) 按需加载。');
|
|
11
|
+
if (['teacher', 'coordinator'].includes(role.kind)) lines.push('简短确认:工具列表提供 ask_user_question 时,学习目标、可用时间、范围或偏好等确需学生补充的信息,优先用原生提问,一次聚焦一件事。用户已说明的信息不重复问;数学练习、推导、费曼讲解等作答仍留在正文与课堂对话,不迁入确认框。原生问题和选项是纯文本,复杂公式先在正文说明。调用示例:ask_user_question({questions:[{id:"focus",question:"今天先学哪一部分?",options:[{label:"导数"},{label:"向量"}]}]});自由回答省略 options,多选用 multi_select:true。默认等待真实回答,不替学生作选择;收到 answers 后按 selected/custom 继续,空答案、取消、pending 或错误都不等于同意或已掌握。若工具未提供,沿用当前对话询问;被父智能体持有的子会话不能调用原生提问,不得绕过边界或反复重试。');
|
|
12
|
+
if (role.kind === 'teacher') lines.push('数学图形与板书:遇到函数变化、几何位置、向量关系等适合用图建立理解的内容,主动使用现有 figure 图,而不只说“想象一张图”。使用前按需加载 skill({name:"notara-quest-tutor"}) 中的画图契约;它包含可运行的函数、几何和 Canvas 示例。图随当前教学步骤展开,复杂推导再落到白板;正式练习只呈现题目已有条件,额外辅助线或解题结构算提示。先核对定义域、关键点和约束;图像观察不是证明,展示也不代表学生掌握。');
|
|
13
|
+
if (role.kind === 'coordinator') lines.push('按需新建独立学习会话时,正文中的 classroom(start) 使用 subagent({preset:"notara-teacher",description:"会话标题",prompt:"知识概况、目标、已确认前置与材料引用"}),创建即返回;简单问题可直接在当前会话回答。要求学生独立尝试时,标题、首页开场语与首轮只呈现客观问题,不提前透露破题方向或关键值;交接注明学生可见内容与内部观察点。多项递进任务按学生节奏分步推进,不必机械限定回合数。沿用现有会话列表,不要求专门入口。worker-* 仍处理一次性材料任务,不传 run_in_background。专题不依赖首页在线,后续需要联系时用 send_message。是否回传、何时回传和内容由会话自己判断,不强制教学进度报告;不要代学生转交答案或把创建成功当成完课。');
|
|
14
|
+
if (role.kind === 'teacher') lines.push('正文中的 worker 改用 subagent 工具的 preset、description、prompt 参数。工作员使用 worker-problem / worker-review / worker-exercise / worker-general / worker-lesson;伙伴使用 companion-deepseek / companion-claude / companion-gemini / companion-gpt。严格盲测必须使用 companion-deepseek-blind,仅传 problem 和 studentExplanation,不传 prompt、参考、共同记忆或 Skill 正文。');
|
|
15
|
+
if (role.kind === 'teacher') lines.push('互动题的题干、步骤和选项也支持 KaTeX。数学表达式用 $...$ 或 $$...$$ 标记,与普通正文一致;不要把 (Qx)^T、x^T 等数学源码直接裸写在题干中。选择题与填空题直接用正文出题,学生在对话中作答;不生成 choice/blank 组件或 HTML 选择、填空控件。混合教学按学生目标和情况组合讲解、提问、示范、练习与反馈:学生要求先自己尝试时先给原题并等待,之后按实际缺口补前置;新概念按需讲清对象、条件与例子,独立练习不泄露关键方向和答案。学生卡住时可主动给一步提示或追问,学生要求完整讲解或仍无法继续时直接讲清。费曼总结和小测按需邀请,不强制作为课尾流程;只把学生实际表达的理解沉淀进 `## 学生理解`。');
|
|
16
|
+
if (role.kind === 'teacher') lines.push('上下文隔离实验:确需单独展开时,可用 subagent({preset:"notara-teacher",description:"讨论标题",prompt:"要讲的知识概况、目标、相关前置与材料"}) 创建原生可持续教学子会话。普通讲解和互动直接在当前课堂进行,不必每个知识点都派出去。没有教学块关系表或教学进度工具;需要时用 send_message 联系父会话或子会话,回不回传、何时回传、内容格式自行判断,不强制汇报。原生 subagent-settled 是本轮运行通知,不代表学生已经掌握或课程完成。');
|
|
17
|
+
if (['teacher', 'coordinator'].includes(role.kind)) lines.push('专题与子会话均复用原生可恢复会话,旧 continuable 历史仍可访问。普通伙伴可以持续接收学生作答;工作员和盲测按限定任务给出本轮结果。后台工作使用原生子会话通知,不是 job_* 任务。原生子会话会自动通知本轮结束,并可能要求向直接父会话发送结果;这只是运行机制,不强制教学进度报告,更不代表学生掌握。没有 classroom(complete) 或 notara.classroom-result.v1 事件。');
|
|
18
|
+
if (['teacher','coordinator','companion'].includes(role.kind)) lines.push('会话通信:source 为 agent-message 的来信由其他会话发出。决定回答时调用 send_message 给消息中标明的发送者,不能用当前窗口正文冒充已回信。告知类消息无需自动回复;发送失败不得声称已送达。正常学生输入仍在当前会话回答,备课答案和内部建议不要混入学生正文。');
|
|
19
|
+
if (['worker', 'companion'].includes(role.kind)) lines.push('当前角色不能再次委派,也不提供 write/edit;需要保存的内容作为草案交还主老师。');
|
|
20
|
+
return lines.join('\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function apply(ctx, { preset }) {
|
|
24
|
+
const role = roleById(preset);
|
|
25
|
+
ctx.systemPrompt.section({
|
|
26
|
+
name: 'deployment:persona-prefix',
|
|
27
|
+
order: ctx.systemPrompt.getSectionOrder('DEPLOYMENT_PERSONA_PREFIX'),
|
|
28
|
+
text: role.prompt + runtimeContract(role),
|
|
29
|
+
interpolate: false,
|
|
30
|
+
complete: true,
|
|
31
|
+
});
|
|
32
|
+
if (role.kind === 'blind') {
|
|
33
|
+
ctx.systemPrompt.suppressRuntimeContext();
|
|
34
|
+
// rc.4 refreshes required contexts again at request admission. Override
|
|
35
|
+
// this provider in the blind scope so suppression also survives refresh.
|
|
36
|
+
ctx.systemPrompt.context({name:'working-directory:current',order:ctx.systemPrompt.getContextOrder('WORKING_DIRECTORY'),required:false,text:'',interpolate:false});
|
|
37
|
+
}
|
|
38
|
+
if (['teacher','coordinator'].includes(role.kind)) ctx.on('system-prompt/assemble',async(assembly,context,next)=>{
|
|
39
|
+
const result=await next(),service=ctx.get('notaraTopics');
|
|
40
|
+
const origin=service&&context.scope?.id?await service.origin(context.scope.id):null;
|
|
41
|
+
const receipt=await readReviewContext(ctx,context);
|
|
42
|
+
return {...result,contexts:[...result.contexts,...origin?[{name:'notara-conversation-origin',text:`本对话由会话 ${origin} 发起。若确有必要,可用 send_message(agent_id:"${origin}",message:"需要告知的内容") 联系它;无需每轮回传,也不要求固定的教学进度格式。`}]:[],...receipt?[receipt]:[]]};
|
|
43
|
+
});
|
|
44
|
+
}
|