@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/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
- 需求不够具体时,先调用 \`ask_user_question\` 一次性问清楚,再动手写页面。不要凭空假设,也不要边写边问。
705
+ **新建网页**时,先调用 \`ask_user_question\` 一次性问清楚,再动手写页面。不要凭空假设,也不要边写边问。
641
706
 
642
- - 何时问:用户只给了一个宽泛主题(如「讲一下光合作用」「做个二次函数的网页」),缺少学段、课型或讲解重点等关键信息时才问。
643
- - 何时不问:需求已经描述清楚、或用户是在修改已有网页,直接开工,不要提问。
644
- - 一次最多问 3 个问题,并在同一次 \`ask_user_question\` 调用里问完,不要分多轮打断老师。
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
- - 字体优先系统中文黑体栈,例如 \`font-family: "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", sans-serif\`;不要使用细体、衬线体或手写体做正文。
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
- - 先定一套配色再写页面:1 个主色 + 1 个辅助色 + 1-2 个强调色,再配中性底色(浅米、浅灰蓝等,不必是纯白)。可参考「深蓝 + 橙」「墨绿 + 暖黄」「深紫 + 青」这类有明确冷暖对比的组合,避免整页同一色相的浅色堆叠。
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 === "" ? "请根据用户提供的图片制作网页。" : 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 };