@zhin.js/plugin-answers 0.0.0-stage → 0.0.2

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.
Files changed (57) hide show
  1. package/README.md +61 -2
  2. package/commands//345/245/275/346/227/240/350/201/212/index.js +18 -0
  3. package/commands//345/245/275/346/227/240/350/201/212/index.ts +17 -0
  4. package/commands//347/247/221/347/233/256/344/270/200/index.js +19 -0
  5. package/commands//347/247/221/347/233/256/344/270/200/index.ts +17 -0
  6. package/commands//347/255/224/346/241/210/344/271/213/344/271/246/index.js +22 -0
  7. package/commands//347/255/224/346/241/210/344/271/213/344/271/246/index.ts +21 -0
  8. package/lib/book-data.d.ts +6 -0
  9. package/lib/book-data.d.ts.map +1 -0
  10. package/lib/book-data.js +87 -0
  11. package/lib/book-data.js.map +1 -0
  12. package/lib/book.d.ts +17 -0
  13. package/lib/book.d.ts.map +1 -0
  14. package/lib/book.js +37 -0
  15. package/lib/book.js.map +1 -0
  16. package/lib/config.d.ts +23 -0
  17. package/lib/config.d.ts.map +1 -0
  18. package/lib/config.js +19 -0
  19. package/lib/config.js.map +1 -0
  20. package/lib/driving-quiz-data.d.ts +11 -0
  21. package/lib/driving-quiz-data.d.ts.map +1 -0
  22. package/lib/driving-quiz-data.js +36 -0
  23. package/lib/driving-quiz-data.js.map +1 -0
  24. package/lib/fun-quiz-data.d.ts +7 -0
  25. package/lib/fun-quiz-data.d.ts.map +1 -0
  26. package/lib/fun-quiz-data.js +47 -0
  27. package/lib/fun-quiz-data.js.map +1 -0
  28. package/lib/index.d.ts +9 -0
  29. package/lib/index.d.ts.map +1 -0
  30. package/lib/index.js +9 -0
  31. package/lib/index.js.map +1 -0
  32. package/lib/quiz-types.d.ts +14 -0
  33. package/lib/quiz-types.d.ts.map +1 -0
  34. package/lib/quiz-types.js +2 -0
  35. package/lib/quiz-types.js.map +1 -0
  36. package/lib/quiz.d.ts +52 -0
  37. package/lib/quiz.d.ts.map +1 -0
  38. package/lib/quiz.js +111 -0
  39. package/lib/quiz.js.map +1 -0
  40. package/lib/runtime.d.ts +4 -0
  41. package/lib/runtime.d.ts.map +1 -0
  42. package/lib/runtime.js +4 -0
  43. package/lib/runtime.js.map +1 -0
  44. package/middlewares/quiz-answer/index.js +21 -0
  45. package/middlewares/quiz-answer/index.ts +22 -0
  46. package/package.json +92 -3
  47. package/plugin.js +21 -0
  48. package/schema.json +41 -0
  49. package/src/book-data.ts +86 -0
  50. package/src/book.ts +48 -0
  51. package/src/config.ts +23 -0
  52. package/src/driving-quiz-data.ts +37 -0
  53. package/src/fun-quiz-data.ts +48 -0
  54. package/src/index.ts +20 -0
  55. package/src/quiz-types.ts +15 -0
  56. package/src/quiz.ts +136 -0
  57. package/src/runtime.ts +5 -0
package/lib/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { BOOK_ANSWERS } from './book-data.js';
2
+ export { BOOK_USAGE, MAX_QUESTION_LENGTH, calendarDay, normalizeQuestion, pickBookAnswer, replyBook } from './book.js';
3
+ export { DEFAULT_ANSWERS_CONFIG, answersConfig, resolveAnswersConfig } from './config.js';
4
+ export { DRIVING_QUESTIONS } from './driving-quiz-data.js';
5
+ export { FUN_QUESTIONS } from './fun-quiz-data.js';
6
+ export { MAX_PENDING_QUIZZES, QuizService, formatJudgement, formatQuestion, parseQuizLetter, pickQuestion, quizKey, } from './quiz.js';
7
+ export { QUIZ_LETTERS } from './quiz-types.js';
8
+ export { quizServiceToken } from './runtime.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,SAAS,EAAsB,MAAM,WAAW,CAAC;AAC3I,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,oBAAoB,EAAsB,MAAM,aAAa,CAAC;AAC9G,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,eAAe,EACf,cAAc,EACd,eAAe,EACf,YAAY,EACZ,OAAO,GAKR,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAsC,MAAM,iBAAiB,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,14 @@
1
+ export type QuizLetter = 'A' | 'B' | 'C' | 'D';
2
+ export interface QuizQuestion {
3
+ /** 题库内唯一、稳定的题号,用于测试与去重。 */
4
+ readonly id: string;
5
+ readonly category: string;
6
+ readonly question: string;
7
+ /** 2–4 个选项,按 A/B/C/D 顺序排列,不含字母前缀。 */
8
+ readonly options: readonly string[];
9
+ readonly answer: QuizLetter;
10
+ /** 作答后展示的解析。 */
11
+ readonly explanation: string;
12
+ }
13
+ export declare const QUIZ_LETTERS: readonly QuizLetter[];
14
+ //# sourceMappingURL=quiz-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quiz-types.d.ts","sourceRoot":"","sources":["../src/quiz-types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,gBAAgB;IAChB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAAyB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export const QUIZ_LETTERS = ['A', 'B', 'C', 'D'];
2
+ //# sourceMappingURL=quiz-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quiz-types.js","sourceRoot":"","sources":["../src/quiz-types.ts"],"names":[],"mappings":"AAcA,MAAM,CAAC,MAAM,YAAY,GAA0B,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC"}
package/lib/quiz.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { type QuizLetter, type QuizQuestion } from './quiz-types.js';
2
+ export type QuizKind = 'fun' | 'driving';
3
+ /** Structural subset of the Runtime / Command message used to scope pending questions. */
4
+ export interface QuizConversationInput {
5
+ readonly conversation?: {
6
+ readonly endpoint?: {
7
+ readonly id?: string;
8
+ readonly adapter?: string;
9
+ };
10
+ readonly kind?: string;
11
+ readonly id?: string;
12
+ readonly threadId?: string;
13
+ };
14
+ readonly sender?: {
15
+ readonly id?: string;
16
+ } | string;
17
+ }
18
+ export interface PendingQuiz {
19
+ readonly kind: QuizKind;
20
+ readonly question: QuizQuestion;
21
+ readonly expiresAt: number;
22
+ }
23
+ export interface QuizJudgement {
24
+ readonly correct: boolean;
25
+ readonly chosen: QuizLetter;
26
+ readonly pending: PendingQuiz;
27
+ }
28
+ /** Upper bound on simultaneously pending questions; the oldest one is dropped first. */
29
+ export declare const MAX_PENDING_QUIZZES = 1000;
30
+ /**
31
+ * 题目只按「同一会话 + 同一发送者」隔离:A 出的题不能由 B 回答,
32
+ * 同一用户在两个群里的题互不影响。返回 undefined 表示缺少可信身份,不出题。
33
+ */
34
+ export declare function quizKey(input: QuizConversationInput | undefined): string | undefined;
35
+ /** 只接受单个选项字母(大小写、全角均可),其余文本一律放行给后续中间件。 */
36
+ export declare function parseQuizLetter(raw: string | undefined): QuizLetter | undefined;
37
+ export declare function pickQuestion(bank: readonly QuizQuestion[], exceptId?: string): QuizQuestion;
38
+ export declare function formatQuestion(kind: QuizKind, question: QuizQuestion, timeoutSeconds: number): string;
39
+ export declare function formatJudgement(judgement: QuizJudgement): string;
40
+ /** Generation-owned, in-memory only: pending questions vanish on reload, which is acceptable for a party game. */
41
+ export declare class QuizService {
42
+ #private;
43
+ ask(key: string, kind: QuizKind, bank: readonly QuizQuestion[], timeoutSeconds: number, now?: number): QuizQuestion;
44
+ peek(key: string, now?: number): PendingQuiz | undefined;
45
+ /** Judge and consume the pending question; letters beyond the option count are ignored (undefined). */
46
+ answer(key: string, letter: QuizLetter, now?: number): QuizJudgement | undefined;
47
+ cancel(key: string): boolean;
48
+ sweep(now?: number): void;
49
+ get size(): number;
50
+ dispose(): void;
51
+ }
52
+ //# sourceMappingURL=quiz.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quiz.d.ts","sourceRoot":"","sources":["../src/quiz.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEnF,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAC;AAEzC,0FAA0F;AAC1F,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE;QACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACxE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC;CACrD;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC/B;AAED,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,qBAAqB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMpF;AAED,0CAA0C;AAC1C,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAI/E;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,YAAY,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAO3F;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAMrG;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM,CAMhE;AAED,kHAAkH;AAClH,qBAAa,WAAW;;IAKtB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,YAAY,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,YAAY;IAavH,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,WAAW,GAAG,SAAS;IAU5D,uGAAuG;IACvG,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,SAAa,GAAG,aAAa,GAAG,SAAS;IASpF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI5B,KAAK,CAAC,GAAG,SAAa,GAAG,IAAI;IAI7B,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,OAAO,IAAI,IAAI;CAShB"}
package/lib/quiz.js ADDED
@@ -0,0 +1,111 @@
1
+ import { randomInt } from 'node:crypto';
2
+ import { QUIZ_LETTERS } from './quiz-types.js';
3
+ /** Upper bound on simultaneously pending questions; the oldest one is dropped first. */
4
+ export const MAX_PENDING_QUIZZES = 1000;
5
+ /**
6
+ * 题目只按「同一会话 + 同一发送者」隔离:A 出的题不能由 B 回答,
7
+ * 同一用户在两个群里的题互不影响。返回 undefined 表示缺少可信身份,不出题。
8
+ */
9
+ export function quizKey(input) {
10
+ const conversation = input?.conversation;
11
+ const sender = typeof input?.sender === 'string' ? input.sender : input?.sender?.id;
12
+ if (!conversation?.id || !conversation.kind || !sender)
13
+ return undefined;
14
+ return JSON.stringify(['answers-quiz-v1', conversation.endpoint?.adapter ?? null, conversation.endpoint?.id ?? null,
15
+ conversation.kind, conversation.id, conversation.threadId ?? null, sender]);
16
+ }
17
+ /** 只接受单个选项字母(大小写、全角均可),其余文本一律放行给后续中间件。 */
18
+ export function parseQuizLetter(raw) {
19
+ const text = (raw ?? '').trim().replace(/[A-Da-d]/gu, (char) => String.fromCharCode(char.charCodeAt(0) - 0xfee0));
20
+ if (!/^[A-Da-d]$/u.test(text))
21
+ return undefined;
22
+ return text.toUpperCase();
23
+ }
24
+ export function pickQuestion(bank, exceptId) {
25
+ if (bank.length === 0)
26
+ throw new Error('题库为空');
27
+ if (bank.length === 1)
28
+ return bank[0];
29
+ let question = bank[randomInt(bank.length)];
30
+ // 避免同一人连续两次抽到同一道题。
31
+ if (question.id === exceptId)
32
+ question = bank[(bank.indexOf(question) + 1 + randomInt(bank.length - 1)) % bank.length];
33
+ return question;
34
+ }
35
+ export function formatQuestion(kind, question, timeoutSeconds) {
36
+ const head = kind === 'fun' ? '😴 好无聊?请听题!' : '🚗 科目一小测,请听题!';
37
+ const options = question.options.map((option, index) => `${QUIZ_LETTERS[index]}. ${option}`);
38
+ const letters = QUIZ_LETTERS.slice(0, question.options.length).join('/');
39
+ return [`${head}【${question.category}】`, '', question.question, '', ...options, '',
40
+ `直接回复 ${letters} 作答,${timeoutSeconds} 秒内有效。`].join('\n');
41
+ }
42
+ export function formatJudgement(judgement) {
43
+ const { question, kind } = judgement.pending;
44
+ const answerText = `${question.answer}. ${question.options[QUIZ_LETTERS.indexOf(question.answer)]}`;
45
+ const verdict = judgement.correct ? '✅ 答对了!' : `❌ 答错了,正确答案是 ${answerText}`;
46
+ const footer = kind === 'driving' ? '\n(原创题面,依据公开法规整理,仅供娱乐参考,以现行规定为准)' : '';
47
+ return `${verdict}\n💡 ${question.explanation}${footer}`;
48
+ }
49
+ /** Generation-owned, in-memory only: pending questions vanish on reload, which is acceptable for a party game. */
50
+ export class QuizService {
51
+ #pending = new Map();
52
+ #lastQuestion = new Map();
53
+ #disposed = false;
54
+ ask(key, kind, bank, timeoutSeconds, now = Date.now()) {
55
+ this.#assertLive();
56
+ this.sweep(now);
57
+ const question = pickQuestion(bank, this.#lastQuestion.get(key));
58
+ this.#pending.delete(key);
59
+ this.#pending.set(key, { kind, question, expiresAt: now + timeoutSeconds * 1000 });
60
+ this.#lastQuestion.delete(key);
61
+ this.#lastQuestion.set(key, question.id);
62
+ while (this.#pending.size > MAX_PENDING_QUIZZES)
63
+ this.#pending.delete(this.#pending.keys().next().value);
64
+ while (this.#lastQuestion.size > MAX_PENDING_QUIZZES)
65
+ this.#lastQuestion.delete(this.#lastQuestion.keys().next().value);
66
+ return question;
67
+ }
68
+ peek(key, now = Date.now()) {
69
+ const pending = this.#pending.get(key);
70
+ if (!pending)
71
+ return undefined;
72
+ if (pending.expiresAt <= now) {
73
+ this.#pending.delete(key);
74
+ return undefined;
75
+ }
76
+ return pending;
77
+ }
78
+ /** Judge and consume the pending question; letters beyond the option count are ignored (undefined). */
79
+ answer(key, letter, now = Date.now()) {
80
+ if (this.#disposed)
81
+ return undefined;
82
+ const pending = this.peek(key, now);
83
+ if (!pending)
84
+ return undefined;
85
+ if (QUIZ_LETTERS.indexOf(letter) >= pending.question.options.length)
86
+ return undefined;
87
+ this.#pending.delete(key);
88
+ return { correct: letter === pending.question.answer, chosen: letter, pending };
89
+ }
90
+ cancel(key) {
91
+ return this.#pending.delete(key);
92
+ }
93
+ sweep(now = Date.now()) {
94
+ for (const [key, pending] of this.#pending)
95
+ if (pending.expiresAt <= now)
96
+ this.#pending.delete(key);
97
+ }
98
+ get size() {
99
+ return this.#pending.size;
100
+ }
101
+ dispose() {
102
+ this.#disposed = true;
103
+ this.#pending.clear();
104
+ this.#lastQuestion.clear();
105
+ }
106
+ #assertLive() {
107
+ if (this.#disposed)
108
+ throw new Error('答题服务属于已停止的插件实例');
109
+ }
110
+ }
111
+ //# sourceMappingURL=quiz.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quiz.js","sourceRoot":"","sources":["../src/quiz.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { QuizService } from './quiz.js';
2
+ /** Owner-scoped quiz state consumed by discovered commands and middleware. */
3
+ export declare const quizServiceToken: import("zhin.js").Token<QuizService>;
4
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,sCAAsG,CAAC"}
package/lib/runtime.js ADDED
@@ -0,0 +1,4 @@
1
+ import { createToken } from 'zhin.js';
2
+ /** Owner-scoped quiz state consumed by discovered commands and middleware. */
3
+ export const quizServiceToken = createToken('zhin.answers.quiz', 'Pending quiz questions per conversation and sender');
4
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGtC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,gBAAgB,GAAG,WAAW,CAAc,mBAAmB,EAAE,oDAAoD,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineMiddleware } from 'zhin.js/middleware';
3
+ import { formatJudgement, parseQuizLetter, quizKey } from "../../lib/quiz.js";
4
+ import { quizServiceToken } from "../../lib/runtime.js";
5
+ /**
6
+ * 只在「发送者本人在本会话有未过期题目」且消息恰好是一个选项字母时接管;
7
+ * 其他消息(包括别人回复的字母)全部交给后续中间件,避免劫持普通聊天。
8
+ */
9
+ export default defineMiddleware({
10
+ target: 'inbound',
11
+ async handle(context, next) {
12
+ const letter = parseQuizLetter(context.input.content);
13
+ const key = letter ? quizKey(context.input) : undefined;
14
+ const judgement = letter && key ? context.use(quizServiceToken).answer(key, letter) : undefined;
15
+ if (!judgement) {
16
+ await next();
17
+ return;
18
+ }
19
+ await context.input.$reply(formatJudgement(judgement));
20
+ },
21
+ });
@@ -0,0 +1,22 @@
1
+ import { defineMiddleware } from 'zhin.js/middleware';
2
+ import type { Message } from '@zhin.js/core/runtime';
3
+ import { formatJudgement, parseQuizLetter, quizKey } from '../../src/quiz.js';
4
+ import { quizServiceToken } from '../../src/runtime.js';
5
+
6
+ /**
7
+ * 只在「发送者本人在本会话有未过期题目」且消息恰好是一个选项字母时接管;
8
+ * 其他消息(包括别人回复的字母)全部交给后续中间件,避免劫持普通聊天。
9
+ */
10
+ export default defineMiddleware<Message>({
11
+ target: 'inbound',
12
+ async handle(context, next) {
13
+ const letter = parseQuizLetter(context.input.content);
14
+ const key = letter ? quizKey(context.input) : undefined;
15
+ const judgement = letter && key ? context.use(quizServiceToken).answer(key, letter) : undefined;
16
+ if (!judgement) {
17
+ await next();
18
+ return;
19
+ }
20
+ await context.input.$reply(formatJudgement(judgement));
21
+ },
22
+ });
package/package.json CHANGED
@@ -1,6 +1,95 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-answers",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.2",
4
+ "description": "Zhin.js 答案之书、趣味选择题与交规小测(原创内容,Plugin Runtime)",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "import": "./lib/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "plugin.js",
17
+ "schema.json",
18
+ "commands",
19
+ "middlewares",
20
+ "src",
21
+ "lib",
22
+ "README.md"
23
+ ],
24
+ "keywords": [
25
+ "zhin",
26
+ "zhin.js",
27
+ "bot",
28
+ "plugin",
29
+ "fun",
30
+ "quiz",
31
+ "answers"
32
+ ],
33
+ "author": {
34
+ "name": "lc-cn",
35
+ "email": "admin@liucl.cn",
36
+ "url": "https://github.com/lc-cn"
37
+ },
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "@zhin.js/core": "1.1.48"
41
+ },
42
+ "devDependencies": {
43
+ "typescript": "^6.0.3",
44
+ "vitest": "^4.1.10",
45
+ "zhin.js": "1.1.13"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/zhinjs/zhin.git",
50
+ "directory": "plugins/utils/answers"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "registry": "https://registry.npmjs.org"
55
+ },
56
+ "engines": {
57
+ "node": ">=22.19.0"
58
+ },
59
+ "zhin": {
60
+ "protocol": 1,
61
+ "type": "plugin",
62
+ "entry": "./plugin.js",
63
+ "engine": "^1.0.0",
64
+ "runtime": "trusted",
65
+ "features": [
66
+ {
67
+ "package": "@zhin.js/command",
68
+ "api": "^1.0.0"
69
+ },
70
+ {
71
+ "package": "@zhin.js/middleware",
72
+ "api": "^1.0.0"
73
+ }
74
+ ],
75
+ "plugins": []
76
+ },
77
+ "peerDependencies": {
78
+ "@zhin.js/command": "^1.1.9",
79
+ "@zhin.js/middleware": "^1.1.8",
80
+ "zhin.js": "^1.1.13"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "@zhin.js/command": {
84
+ "optional": true
85
+ },
86
+ "@zhin.js/middleware": {
87
+ "optional": true
88
+ }
89
+ },
90
+ "scripts": {
91
+ "build": "tsc",
92
+ "clean": "rimraf lib",
93
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/utils/answers/tests"
94
+ }
6
95
  }
package/plugin.js ADDED
@@ -0,0 +1,21 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { definePlugin } from 'zhin.js';
3
+ import { QuizService } from "./lib/quiz.js";
4
+ import { quizServiceToken } from "./lib/runtime.js";
5
+ /**
6
+ * Plugin Runtime:
7
+ * - `commands/` 提供 答案之书 / 好无聊 / 科目一;
8
+ * - `middlewares/quiz-answer` 只接管「有待答题目的同一发送者」回复的单个选项字母;
9
+ * - 题目状态仅在内存中保存,随插件代际销毁。
10
+ */
11
+ export default definePlugin({
12
+ name: 'answers',
13
+ metadata: {
14
+ displayName: '答案之书',
15
+ },
16
+ setup(context) {
17
+ const service = new QuizService();
18
+ context.resources.provide(quizServiceToken, service);
19
+ return () => service.dispose();
20
+ },
21
+ });
package/schema.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "type": "object",
3
+ "additionalProperties": false,
4
+ "properties": {
5
+ "dailyStable": {
6
+ "type": "boolean",
7
+ "description": "同一用户同一天问同一个问题时,答案之书给出相同答案",
8
+ "default": false,
9
+ "x-zhin-config": {
10
+ "effect": "reload"
11
+ }
12
+ },
13
+ "timezone": {
14
+ "type": "string",
15
+ "description": "dailyStable 判断「同一天」所用的 IANA 时区",
16
+ "default": "Asia/Shanghai",
17
+ "x-zhin-config": {
18
+ "effect": "reload"
19
+ }
20
+ },
21
+ "quizTimeoutSeconds": {
22
+ "type": "number",
23
+ "description": "出题后等待作答的秒数,超时题目作废",
24
+ "default": 120,
25
+ "minimum": 15,
26
+ "maximum": 600,
27
+ "multipleOf": 1,
28
+ "x-zhin-config": {
29
+ "effect": "reload"
30
+ }
31
+ },
32
+ "enableDrivingQuiz": {
33
+ "type": "boolean",
34
+ "description": "启用「科目一」交规小测(原创题面,仅供娱乐参考)",
35
+ "default": true,
36
+ "x-zhin-config": {
37
+ "effect": "reload"
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * 答案之书的回答。全部为本仓库原创短句(MIT),不来自任何第三方题库或书籍。
3
+ * 措辞刻意保持含糊、积极或俏皮,避免给出医疗、法律、投资等具体建议。
4
+ */
5
+ export const BOOK_ANSWERS: readonly string[] = Object.freeze([
6
+ '可以,但别急着今天就做完。',
7
+ '先睡一觉,明早再看,答案会更清楚。',
8
+ '是的,而且比你想的更顺利。',
9
+ '不是现在。',
10
+ '换个角度,问题就变小了。',
11
+ '去做吧,最坏也不过是多一个故事。',
12
+ '答案藏在你第一反应里。',
13
+ '再等一等,时机还没熟。',
14
+ '值得一试。',
15
+ '别勉强,放过自己也是一种选择。',
16
+ '问问最了解你的那个人。',
17
+ '这件事比你想的简单。',
18
+ '这件事比你想的复杂,慢慢来。',
19
+ '先把手头的小事做好。',
20
+ '会的。',
21
+ '大概率不会,但不妨碍你努力。',
22
+ '把它写下来,你就知道了。',
23
+ '你心里已经有答案了。',
24
+ '保持好奇,别急着下结论。',
25
+ '随缘,但别躺平。',
26
+ '先吃点好的再决定。',
27
+ '这一次,听从直觉。',
28
+ '这一次,相信数据。',
29
+ '请把期待值调低一点点。',
30
+ '请把期待值调高一点点。',
31
+ '机会就在下一个路口。',
32
+ '不必向所有人解释。',
33
+ '答案是肯定的,只是需要一点耐心。',
34
+ '答案是否定的,但下一扇门已经开了。',
35
+ '先迈出一小步。',
36
+ '别回头,往前看。',
37
+ '回头看看,你漏掉了什么。',
38
+ '换一种问法再问一次。',
39
+ '今天不适合做重大决定。',
40
+ '今天很适合开始。',
41
+ '交给时间。',
42
+ '少想,多做。',
43
+ '多想,少做。',
44
+ '你需要的是休息,不是答案。',
45
+ '和朋友聊聊,会有新思路。',
46
+ '小心细节。',
47
+ '别和自己较劲。',
48
+ '是时候做出改变了。',
49
+ '保持现状也很好。',
50
+ '你会为这个决定感到高兴。',
51
+ '先把预算算清楚。',
52
+ '相信过程。',
53
+ '未来的你会感谢现在的坚持。',
54
+ '答案会在不经意间出现。',
55
+ '这不是你一个人的问题。',
56
+ '放轻松,事情没那么糟。',
57
+ '大胆一点。',
58
+ '谨慎一点。',
59
+ '把选择权留给自己。',
60
+ '先问问钱包的意见。',
61
+ '去散个步,回来就懂了。',
62
+ '别被别人的节奏打乱。',
63
+ '耐心是这道题的解法。',
64
+ '是的,但要付出代价。',
65
+ '不,但你会学到东西。',
66
+ '它比看起来更重要。',
67
+ '它没有看起来那么重要。',
68
+ '答案在路上,请查收。',
69
+ '先喝杯水。',
70
+ '用你最擅长的方式去做。',
71
+ '试着去请教一个陌生人。',
72
+ '这是个好问题,值得认真对待。',
73
+ '这个问题,三天后再问。',
74
+ '不要让恐惧替你做决定。',
75
+ '不要让冲动替你做决定。',
76
+ '星星说:可以。',
77
+ '星星说:再想想。',
78
+ '一切都会好起来的。',
79
+ '你比你想象的更有能力。',
80
+ '慢慢来,比较快。',
81
+ '把注意力放在能控制的事情上。',
82
+ '先完成,再完美。',
83
+ '有时候,不做也是一种答案。',
84
+ '今天的运气站在你这边。',
85
+ '书页被风吹乱了,请稍后再问。',
86
+ ]);
package/src/book.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { createHash, randomInt } from 'node:crypto';
2
+ import { BOOK_ANSWERS } from './book-data.js';
3
+
4
+ export const BOOK_USAGE = [
5
+ '📖 答案之书',
6
+ '用法:答案之书 <问题>',
7
+ '例如:答案之书 今天要不要出去走走',
8
+ '心里默念问题后发送,书会翻开一页给你答案。',
9
+ ].join('\n');
10
+
11
+ export const MAX_QUESTION_LENGTH = 200;
12
+
13
+ export interface BookPickInput {
14
+ readonly question: string;
15
+ readonly senderId: string;
16
+ /** 为 true 时,同一用户同一天的同一问题得到相同答案。 */
17
+ readonly dailyStable: boolean;
18
+ readonly timezone: string;
19
+ readonly now?: number;
20
+ }
21
+
22
+ /** 规范化问题:去掉空白与常见标点,避免「?」和「?」得到不同答案。 */
23
+ export function normalizeQuestion(question: string): string {
24
+ return question.trim().toLowerCase().replace(/[\s\p{P}]+/gu, '');
25
+ }
26
+
27
+ /** 给定时区下的日历日期(YYYY-MM-DD);时区无效时退回 UTC。 */
28
+ export function calendarDay(now: number, timezone: string): string {
29
+ try {
30
+ return new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
31
+ } catch {
32
+ return new Date(now).toISOString().slice(0, 10);
33
+ }
34
+ }
35
+
36
+ export function pickBookAnswer(input: BookPickInput, answers: readonly string[] = BOOK_ANSWERS): string {
37
+ if (answers.length === 0) throw new Error('答案之书为空');
38
+ if (!input.dailyStable) return answers[randomInt(answers.length)]!;
39
+ const day = calendarDay(input.now ?? Date.now(), input.timezone);
40
+ const digest = createHash('sha256')
41
+ .update(JSON.stringify(['answers-book-v1', day, input.senderId, normalizeQuestion(input.question)]))
42
+ .digest();
43
+ return answers[digest.readUInt32BE(0) % answers.length]!;
44
+ }
45
+
46
+ export function replyBook(question: string, answer: string): string {
47
+ return `📖 你问:${question}\n✨ 答案之书:${answer}`;
48
+ }
package/src/config.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { Schema } from 'zhin.js';
2
+
3
+ export const DEFAULT_ANSWERS_CONFIG = Object.freeze({
4
+ dailyStable: false,
5
+ timezone: 'Asia/Shanghai',
6
+ quizTimeoutSeconds: 120,
7
+ enableDrivingQuiz: true,
8
+ });
9
+
10
+ /** Authoring declaration; scripts/sync-plugin-config-schemas.mjs generates the packaged schema.json for Runtime and Console. */
11
+ export const answersConfig = Schema.object({
12
+ dailyStable: Schema.boolean().default(DEFAULT_ANSWERS_CONFIG.dailyStable).description('同一用户同一天问同一个问题时,答案之书给出相同答案').effect('reload'),
13
+ timezone: Schema.string().default(DEFAULT_ANSWERS_CONFIG.timezone).description('dailyStable 判断「同一天」所用的 IANA 时区').effect('reload'),
14
+ quizTimeoutSeconds: Schema.number().min(15).max(600).step(1).default(DEFAULT_ANSWERS_CONFIG.quizTimeoutSeconds).description('出题后等待作答的秒数,超时题目作废').effect('reload'),
15
+ enableDrivingQuiz: Schema.boolean().default(DEFAULT_ANSWERS_CONFIG.enableDrivingQuiz).description('启用「科目一」交规小测(原创题面,仅供娱乐参考)').effect('reload'),
16
+ });
17
+
18
+ export type AnswersConfig = Schema.Output<typeof answersConfig>;
19
+
20
+ /** Missing fields fall back to defaults so commands stay usable when Runtime passes a partial view. */
21
+ export function resolveAnswersConfig(config: Partial<AnswersConfig> | undefined): AnswersConfig {
22
+ return { ...DEFAULT_ANSWERS_CONFIG, ...(config ?? {}) } as AnswersConfig;
23
+ }