@houser/edus-plugin 1.0.8 → 1.0.9
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 +13 -3
- package/cordis.yml +73 -0
- package/lib/client.js +16140 -1032
- package/lib/index.js +114 -26
- package/lib/typert.host.js +429 -115
- package/lib/typert.remote-client.d.ts +22 -2
- package/lib/typert.remote-client.js +423 -114
- package/lib/types/auth.d.ts +3 -0
- package/lib/types/client/HtmlWorkbench.d.ts +1 -0
- package/lib/types/client/LoginGuide.d.ts +33 -0
- package/lib/types/llm-gateway.d.ts +14 -0
- package/lib/types/types.d.ts +14 -0
- package/lib/types/webpage-command.d.ts +8 -0
- package/lib/types/webpage-instruction.d.ts +1 -1
- package/package.json +42 -47
- package/lib/types/invariant.d.ts +0 -5
package/lib/index.js
CHANGED
|
@@ -2,8 +2,49 @@ import z from "@deepseek-ai/schemastery";
|
|
|
2
2
|
import { basename, extname } from "node:path";
|
|
3
3
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
4
4
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
5
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
5
6
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
6
7
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
8
|
+
//#region lib/types/llm-gateway.js
|
|
9
|
+
/**
|
|
10
|
+
* 模型网关的密钥就是 ai-edu 登录 token。cordis.yml 里 llm-pi-ai 路由的 apiKeyEnv
|
|
11
|
+
* 指向这个引用,适配器每次请求都重新解析,所以写入或删除后下一次请求即生效。
|
|
12
|
+
*/
|
|
13
|
+
const LLM_TOKEN_REF = credentialRef("EDUS_LLM_TOKEN");
|
|
14
|
+
/** cordis.yml 中指向网关的 pi-ai 路由名。 */
|
|
15
|
+
const GATEWAY_PROVIDER = "deepseek-official";
|
|
16
|
+
const LOGIN_REQUIRED = {
|
|
17
|
+
type: "finish",
|
|
18
|
+
reason: {
|
|
19
|
+
kind: "error",
|
|
20
|
+
failure: {
|
|
21
|
+
code: "MISSING_CREDENTIAL",
|
|
22
|
+
message: "请先登录课成账号,再开始对话"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* 未登录时 pi-ai 只会报一条英文的 MISSING_CREDENTIAL,教会用户去设置页填密钥,
|
|
28
|
+
* 而这个入口已被屏蔽。这里在 llm/stream 瀑布上先查凭据,缺失就直接给出终止分片,
|
|
29
|
+
* 不触达适配器;已登录则原样放行。
|
|
30
|
+
*/
|
|
31
|
+
function applyLlmGateway(ctx) {
|
|
32
|
+
ctx.inject(["llm"], (child) => {
|
|
33
|
+
child.on("llm/stream", (options, next) => {
|
|
34
|
+
if (options.provider !== GATEWAY_PROVIDER) return next();
|
|
35
|
+
return gated(child, next);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async function* gated(ctx, next) {
|
|
40
|
+
const credentials = ctx.get("credentials");
|
|
41
|
+
if (credentials !== void 0 && await credentials.resolve(LLM_TOKEN_REF) === void 0) {
|
|
42
|
+
yield LOGIN_REQUIRED;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
yield* next();
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
7
48
|
//#region lib/types/marker.js
|
|
8
49
|
const MARKER = /<!--\s*edus-publish:(.*?)-->\s*/gs;
|
|
9
50
|
function extractMarker(content) {
|
|
@@ -93,7 +134,14 @@ function login(wire) {
|
|
|
93
134
|
isAdmin: wire.is_admin ?? false,
|
|
94
135
|
isVip: wire.is_vip ?? false,
|
|
95
136
|
title: wire.title,
|
|
96
|
-
vipLevel: wire.vip_level
|
|
137
|
+
vipLevel: wire.vip_level,
|
|
138
|
+
vipLevelName: wire.vip_level_name ?? "",
|
|
139
|
+
vipMonthLimit: wire.vip_month_limit ?? 0,
|
|
140
|
+
vipEndTime: wire.vip_end_time ?? null,
|
|
141
|
+
llmStatus: wire.llm_status ?? null,
|
|
142
|
+
llmTotalCost: wire.llm_total_cost ?? "",
|
|
143
|
+
llmMonthCost: wire.llm_month_cost ?? "",
|
|
144
|
+
llmLastUsedAt: wire.llm_last_used_at ?? null
|
|
97
145
|
};
|
|
98
146
|
}
|
|
99
147
|
async function publicCall(base, path, body) {
|
|
@@ -152,6 +200,7 @@ let EdusAuthService = (() => {
|
|
|
152
200
|
let _classSuper = TypertRemoteService;
|
|
153
201
|
let _instanceExtraInitializers = [];
|
|
154
202
|
let _shareOrigin_decorators;
|
|
203
|
+
let _setLlmToken_decorators;
|
|
155
204
|
let _login_decorators;
|
|
156
205
|
let _loginInfo_decorators;
|
|
157
206
|
let _sendPhoneCode_decorators;
|
|
@@ -170,6 +219,7 @@ let EdusAuthService = (() => {
|
|
|
170
219
|
static {
|
|
171
220
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
172
221
|
_shareOrigin_decorators = [Remote("shareOrigin")];
|
|
222
|
+
_setLlmToken_decorators = [Remote("setLlmToken")];
|
|
173
223
|
_login_decorators = [Remote("login")];
|
|
174
224
|
_loginInfo_decorators = [Remote("loginInfo")];
|
|
175
225
|
_sendPhoneCode_decorators = [Remote("sendPhoneCode")];
|
|
@@ -195,6 +245,17 @@ let EdusAuthService = (() => {
|
|
|
195
245
|
},
|
|
196
246
|
metadata: _metadata
|
|
197
247
|
}, null, _instanceExtraInitializers);
|
|
248
|
+
__esDecorate(this, null, _setLlmToken_decorators, {
|
|
249
|
+
kind: "method",
|
|
250
|
+
name: "setLlmToken",
|
|
251
|
+
static: false,
|
|
252
|
+
private: false,
|
|
253
|
+
access: {
|
|
254
|
+
has: (obj) => "setLlmToken" in obj,
|
|
255
|
+
get: (obj) => obj.setLlmToken
|
|
256
|
+
},
|
|
257
|
+
metadata: _metadata
|
|
258
|
+
}, null, _instanceExtraInitializers);
|
|
198
259
|
__esDecorate(this, null, _login_decorators, {
|
|
199
260
|
kind: "method",
|
|
200
261
|
name: "login",
|
|
@@ -383,9 +444,19 @@ let EdusAuthService = (() => {
|
|
|
383
444
|
const session = this.ctx.sessions.get(SessionId(sessionId));
|
|
384
445
|
return policy?.resolve(session === void 0 ? {} : { session });
|
|
385
446
|
}
|
|
447
|
+
credentials() {
|
|
448
|
+
const credentials = this.ctx.get("credentials");
|
|
449
|
+
if (credentials === void 0) throw new Error("凭据存储不可用,无法配置模型服务");
|
|
450
|
+
return credentials;
|
|
451
|
+
}
|
|
386
452
|
shareOrigin() {
|
|
387
453
|
return Promise.resolve(this.baseUrl);
|
|
388
454
|
}
|
|
455
|
+
/** 客户端登录态变化时同步:有 token 就写入模型网关密钥,退出登录则删除。 */
|
|
456
|
+
async setLlmToken(token) {
|
|
457
|
+
if (token.trim() === "") await this.credentials().unset(LLM_TOKEN_REF);
|
|
458
|
+
else await this.credentials().set(LLM_TOKEN_REF, token);
|
|
459
|
+
}
|
|
389
460
|
async login(phone, password) {
|
|
390
461
|
return login(await publicCall(this.baseUrl, "/api/login/login_by_password", {
|
|
391
462
|
phone,
|
|
@@ -570,13 +641,7 @@ let EdusAuthService = (() => {
|
|
|
570
641
|
//#endregion
|
|
571
642
|
//#region lib/types/publish-image-tool.js
|
|
572
643
|
function findImage(blocks, id) {
|
|
573
|
-
for (const block of blocks)
|
|
574
|
-
if (block.type === "image" && String(block.attachment.attachmentId) === id) return block.attachment;
|
|
575
|
-
if (block.type === "tool-result") {
|
|
576
|
-
const found = findImage(block.content, id);
|
|
577
|
-
if (found !== void 0) return found;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
644
|
+
for (const block of blocks) if (block.type === "image" && String(block.attachment.attachmentId) === id) return block.attachment;
|
|
580
645
|
}
|
|
581
646
|
/** Publish only images already attached to the calling conversation. */
|
|
582
647
|
function applyPublishImageTool(ctx, baseUrl) {
|
|
@@ -637,22 +702,45 @@ const WEBPAGE_INSTRUCTION = `# 网页生成规则
|
|
|
637
702
|
|
|
638
703
|
## 开工前的确认
|
|
639
704
|
|
|
640
|
-
|
|
705
|
+
**新建网页**时,先调用 \`ask_user_question\` 一次性问清楚,再动手写页面。不要凭空假设,也不要边写边问。
|
|
641
706
|
|
|
642
|
-
-
|
|
643
|
-
-
|
|
644
|
-
- 一次最多问
|
|
645
|
-
- 每个问题给 2-4 个具体选项:最可能的放第一个并在标签末尾加「(推荐)」,每个选项配一句话说明区别。\`header\`
|
|
707
|
+
- 何时问:新建网页时都要问。用户只给了宽泛主题(如「讲一下光合作用」「做个二次函数的网页」)时,问教学问题 + 视觉风格;用户已把教学需求讲清楚时,只问视觉风格这一个问题。
|
|
708
|
+
- 何时不问:用户是在修改已有网页,或者教学需求和风格都已经说明白(例如「做一个黑板风的……」),直接开工,不要提问。
|
|
709
|
+
- 一次最多问 4 个问题(其中视觉风格固定占 1 个),并在同一次 \`ask_user_question\` 调用里问完,不要分多轮打断老师。
|
|
710
|
+
- 每个问题给 2-4 个具体选项:最可能的放第一个并在标签末尾加「(推荐)」,每个选项配一句话说明区别。\`header\` 用「学段」「课型」「风格」这类简短中文词。
|
|
646
711
|
- 问教学问题,用老师的语言。可以问:面向哪个学段年级、这页用于新课讲解还是复习巩固、重点讲哪个环节、要不要配练习题。
|
|
647
|
-
-
|
|
648
|
-
-
|
|
712
|
+
- 问视觉风格时:从下方「视觉风格」风格库里挑 4 个最适合本学科、本学段的风格作为选项,把最贴合的放第一个并标「(推荐)」。选项说明用老师能想象出画面的话描述(如「深绿黑板底配粉笔色字,像在黑板上板书」),不要写色值和技术术语。不要每次都给同一组选项。
|
|
713
|
+
- 除视觉风格外,不要问技术问题:具体色值、字体、动画实现方式、是否引入公式库等一律按本规则自行决定,不许拿来问用户。
|
|
714
|
+
- 拿到回答后直接按回答生成,不要再确认一遍。用户跳过问题时,按最常见的课堂情况取默认值继续做;跳过风格时按学科学段自行从风格库挑一个最贴合的,不要追问。
|
|
715
|
+
|
|
716
|
+
## 视觉风格
|
|
717
|
+
|
|
718
|
+
不同网页之间要有明显不同的视觉个性,**不要每页都做成同一种「白底 + 彩色圆角卡片」的模板**。选定风格后,背景、标题样式、卡片形态、分隔方式、图示画法、按钮造型都要统一服从这个风格,而不只是换一下主色。
|
|
719
|
+
|
|
720
|
+
风格库(每种都已考虑投影可读性,标注的是适用场景,供挑选选项时参考):
|
|
721
|
+
|
|
722
|
+
- **黑板板书**:深墨绿或深灰黑板底,正文白色,重点用粉笔黄、粉笔粉、粉笔蓝;标题下用手绘感粗线,图示用粗白线描边。适合数学推导、理科讲解,各学段通用。
|
|
723
|
+
- **科技深色**:深海军蓝底,青色、电光紫做强调,细网格或坐标纹理背景,卡片用描边发光边框,数据和图示像仪表盘。适合物理、信息技术、天文、高年级理科。
|
|
724
|
+
- **童趣活泼**:高饱和的红黄蓝绿撞色,大圆角、粗描边、贴纸感卡片,大号图标和卡通化示意图,按钮做成胶囊形。适合小学低中年级各科。
|
|
725
|
+
- **国风雅致**:宣纸米黄底,朱砂红、黛青、墨黑为主色,标题配印章式色块,分隔用细回纹或竖排小标题点缀(正文仍横排)。适合语文古诗文、历史、传统文化、美术。
|
|
726
|
+
- **自然清新**:浅草绿或浅天蓝底,墨绿 + 暖橙配色,叶片、水滴形状的图标与分区,卡片带柔和阴影。适合生物、地理、科学、环境主题。
|
|
727
|
+
- **笔记手账**:方格纸或横线本底纹,荧光笔式高亮条(黄、绿、粉)标重点,便签贴纸样式卡片,箭头与圈注强调。适合复习巩固、知识梳理、英语词汇。
|
|
728
|
+
- **杂志排版**:大幅色块分区、超大号标题与数字、左右不对称版式、强对比的黑 + 一个高饱和主色(如正红、明黄)。适合初高中文科、专题探究、主题班会。
|
|
729
|
+
- **实验报告**:浅灰蓝底,表格、流程图、步骤编号是主要元素,用蓝 + 橙区分「操作」与「现象 / 结论」,图示像实验装置示意图。适合物理化学实验课、探究课。
|
|
730
|
+
|
|
731
|
+
使用要求:
|
|
732
|
+
|
|
733
|
+
- 风格只改变视觉气质,不放宽可读性底线:下方「排版与字体」「配色」中的字号、字重、对比度规则在任何风格下都必须遵守(深色风格同样要满足深底浅字的对比要求)。
|
|
734
|
+
- 风格化元素(纹理、印章、贴纸、手绘线)只能放在背景、边框、标题装饰和图示上,不得压在正文文字下面降低清晰度。
|
|
735
|
+
- 正文字体始终用系统黑体栈;标题可以按风格选用更有个性的中文字体(如国风用宋体 / 楷体加粗、童趣用圆体),但字重不低于 \`700\`,并保证投屏能看清。
|
|
736
|
+
- 用户选「其他」并自己描述风格时,按用户描述设计,同样遵守可读性底线。
|
|
649
737
|
|
|
650
738
|
## 排版与字体
|
|
651
739
|
|
|
652
740
|
- 正文字号不小于 \`22px\`,推荐 \`24-28px\`;图注、标签、表格等次要文字不小于 \`18px\`,不得出现小于 \`16px\` 的任何文字。
|
|
653
741
|
- 标题要有明显层级差:主标题 \`48-64px\`,章节标题 \`36-44px\`,小节标题 \`28-32px\`。
|
|
654
742
|
- 字重要足:正文用 \`500\`,标题用 \`700\` 及以上。禁止使用 \`100/200/300\` 等细字重,禁止给中文设 \`font-weight: 300\` 或 \`lighter\`。
|
|
655
|
-
-
|
|
743
|
+
- 正文字体用系统中文黑体栈,例如 \`font-family: "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", sans-serif\`;不要使用细体、衬线体或手写体做正文。标题字体可按视觉风格调整。
|
|
656
744
|
- 行高 \`1.7-1.9\`,段落之间留足空白;正文容器宽度控制在 \`1100-1300px\` 居中,一行不要太长。
|
|
657
745
|
- 交互控件(按钮、滑块、输入框)要大:按钮高度不小于 \`48px\`,文字不小于 \`20px\`,便于老师在讲台上快速点击。
|
|
658
746
|
- 公式、代码、图表标注同样要放大,不要沿用默认小字号。
|
|
@@ -662,10 +750,10 @@ const WEBPAGE_INSTRUCTION = `# 网页生成规则
|
|
|
662
750
|
|
|
663
751
|
课堂网页要有鲜明的视觉主题,**不要做成黑白灰的纯文档**。配色的目标是帮助学生快速区分内容结构,不是为了克制。
|
|
664
752
|
|
|
665
|
-
-
|
|
753
|
+
- 先按选定的视觉风格定一套配色再写页面:1 个主色 + 1 个辅助色 + 1-2 个强调色,再配与风格相符的底色(浅色风格用浅米、浅灰蓝等,不必是纯白;深色风格用深墨绿、深海军蓝等)。配色要有明确的冷暖或明暗对比,避免整页同一色相的浅色堆叠。
|
|
666
754
|
- 颜色用在大面积元素上:分区背景、标题条、卡片、图示、表头、边框、图标、进度条、重点底纹。正文文字本身保持深色高对比,不要靠给文字上色来做视觉效果。
|
|
667
755
|
- 用颜色表达结构:不同章节可用不同主题色;定义、例题、重点、易错点、小结用不同颜色的卡片区分,让学生一眼看出「这是哪一类内容」。
|
|
668
|
-
-
|
|
756
|
+
- 允许渐变、色块、卡片、阴影、纹理、大号图标和示意图,具体形态跟随视觉风格,只要不压低文字清晰度。
|
|
669
757
|
- 投影会让颜色整体变浅、对比度下降,所以配色要够饱和:避免大面积浅粉、浅黄、浅灰蓝这类投在墙上几乎分不出层次的颜色,也避免相邻区块颜色过于接近。
|
|
670
758
|
- 文字对比度底线:浅底深字时正文不浅于 \`#222\`、次要文字不浅于 \`#555\`;深底浅字时正文不深于 \`#EEE\`。彩色底上放文字时,用深色底配白字或很浅的底配深色字,不要中等明度底配中等明度字。禁止 \`#999\` 及更浅的颜色承载正文,禁止半透明文字,禁止彩色细字重小字。
|
|
671
759
|
|
|
@@ -760,7 +848,7 @@ const WEBPAGE_INSTRUCTION = `# 网页生成规则
|
|
|
760
848
|
## 自检范围
|
|
761
849
|
|
|
762
850
|
- 只做静态检查:确认没有本地资源引用、标签闭合、脚本没有明显语法错误。一条命令完成,不要反复检查。
|
|
763
|
-
- 同时检查排版、配色与文案:有没有小于 \`16px\` 的字号、有没有 \`300\`
|
|
851
|
+
- 同时检查排版、配色与文案:有没有小于 \`16px\` 的字号、有没有 \`300\` 及以下的字重、有没有英文装饰文案、页面是不是只有黑白灰、整体是否体现了选定的视觉风格。发现就直接改掉。
|
|
764
852
|
- 用到公式时,检查是否引入了 MathJax 且配置了 \`inlineMath\`,公式定界符是否成对。
|
|
765
853
|
- 有练习题时,确认答案默认处于隐藏状态,且每题都有可点击的展开按钮。
|
|
766
854
|
- 用户没有明确要求时,不要启动无头浏览器、不要截图、不要注入探针脚本跑页面自测、不要回看渲染结果图片。
|
|
@@ -783,9 +871,9 @@ function applyWebpageCommand(ctx) {
|
|
|
783
871
|
ctx.inject(["commands"], (commandCtx) => {
|
|
784
872
|
commandCtx.commands.register({
|
|
785
873
|
name: "generate-webpage",
|
|
786
|
-
description: "
|
|
874
|
+
description: "制作教学网页:根据教学需求和配图生成课堂展示网页",
|
|
787
875
|
input: {
|
|
788
|
-
hint: "[
|
|
876
|
+
hint: "[教学网页需求]",
|
|
789
877
|
attachments: true
|
|
790
878
|
},
|
|
791
879
|
recordInput: false,
|
|
@@ -793,7 +881,7 @@ function applyWebpageCommand(ctx) {
|
|
|
793
881
|
const request = rawInput.trim();
|
|
794
882
|
if (request === "" && attachments.length === 0) return {
|
|
795
883
|
kind: "error",
|
|
796
|
-
text: "请在 /generate-webpage
|
|
884
|
+
text: "请在 /generate-webpage 后描述教学网页的需求,或附上要使用的图片。"
|
|
797
885
|
};
|
|
798
886
|
agent.inject(createUserMessage({
|
|
799
887
|
content: [{
|
|
@@ -801,21 +889,20 @@ function applyWebpageCommand(ctx) {
|
|
|
801
889
|
text: WEBPAGE_INSTRUCTION
|
|
802
890
|
}],
|
|
803
891
|
source: {
|
|
804
|
-
kind: "plugin",
|
|
805
|
-
plugin: "edus-plugin",
|
|
892
|
+
kind: "edus-plugin",
|
|
806
893
|
form: "instructions"
|
|
807
894
|
}
|
|
808
895
|
}));
|
|
809
896
|
agent.steer(createUserMessage({
|
|
810
897
|
content: [...attachments, {
|
|
811
898
|
type: "text",
|
|
812
|
-
text: request === "" ? "
|
|
899
|
+
text: request === "" ? "请根据用户提供的图片制作教学网页。" : request
|
|
813
900
|
}],
|
|
814
901
|
source: { kind: "user" }
|
|
815
902
|
}));
|
|
816
903
|
return {
|
|
817
904
|
kind: "success",
|
|
818
|
-
text: "
|
|
905
|
+
text: "已提交教学网页制作需求。"
|
|
819
906
|
};
|
|
820
907
|
}
|
|
821
908
|
});
|
|
@@ -831,6 +918,7 @@ function apply(ctx, config) {
|
|
|
831
918
|
ctx.plugin(EdusAuthService, baseUrl);
|
|
832
919
|
applyPublishImageTool(ctx, baseUrl);
|
|
833
920
|
applyWebpageCommand(ctx);
|
|
921
|
+
applyLlmGateway(ctx);
|
|
834
922
|
}
|
|
835
923
|
//#endregion
|
|
836
924
|
export { Config, EdusAuthService, apply, inject, name };
|