@quantacenter/quanta-problem-cli 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .: {}
@@ -0,0 +1,28 @@
1
+ import { createServer } from 'node:http';
2
+
3
+ // 答案模板:实现待办清单 HTTP API(GET /health → 200 {ok:true};GET /items → 200 {items:[]};POST /items → 201 {item},title 为空 400),改完用 curl 自验。
4
+ const sendJson = (res, status, body) => {
5
+ const payload = JSON.stringify(body);
6
+ res.writeHead(status, {
7
+ 'content-type': 'application/json; charset=utf-8',
8
+ 'content-length': Buffer.byteLength(payload),
9
+ });
10
+ res.end(payload);
11
+ };
12
+
13
+ const server = createServer((req, res) => {
14
+ const url = new URL(req.url ?? '/', 'http://localhost');
15
+
16
+ if (req.method === 'GET' && url.pathname === '/health') {
17
+ return sendJson(res, 200, { ok: true });
18
+ }
19
+
20
+ // TODO: 实现 GET /items 与 POST /items
21
+
22
+ return sendJson(res, 404, { error: 'not_found' });
23
+ });
24
+
25
+ const port = Number(process.env.PORT || process.env.QUANTA_PORT || 3000);
26
+ server.listen(port, () => {
27
+ console.log('todo-api listening on ' + port);
28
+ });
@@ -0,0 +1,34 @@
1
+ # {{title}}
2
+
3
+ 本目录由 `qpc init` 生成,包含出题所需的全部文件。
4
+
5
+ ## 目录结构
6
+
7
+ ```
8
+ problem.config.ts 题目配置(标题、难度、标签、运行命令)
9
+ judge.js 判题脚本(真源;判分逻辑都在这里)
10
+ template/ 答案模板:学生从这里开始,通常拿低分
11
+ answer/ 参考解:必须拿满分,首屏截图会当作封面
12
+ ```
13
+
14
+ ## 标准流程
15
+
16
+ ```bash
17
+ # 1. 本地预检(毫秒级,先挡掉静态错误)
18
+ qpc check
19
+
20
+ # 2. 上传并等待审计(生成题目版本,审计约 20 秒)
21
+ qpc upload --wait
22
+
23
+ # 3. 审计通过后发布
24
+ qpc publish <problemId>
25
+ ```
26
+
27
+ ## 需要人工确认的事(预检覆盖不到)
28
+
29
+ - [ ] 参考解真的能拿满分(看 `qpc upload --wait` 的检查点明细)
30
+ - [ ] 答案模板明显低分(证明判题能抓错)
31
+ - [ ] 断言消息写清"期望 vs 实际"(预检只做启发式提醒)
32
+ - [ ] 封面好看(默认封面 = 参考解首屏截图)
33
+
34
+ 更完整的踩坑清单见仓库 `docs/PROBLEM_AUTHORING.md`。
@@ -0,0 +1,97 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{title}}</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
11
+ }
12
+ body {
13
+ margin: 0;
14
+ min-height: 100vh;
15
+ display: grid;
16
+ place-items: center;
17
+ background: #f6f7f9;
18
+ color: #1f2328;
19
+ }
20
+ .card {
21
+ width: min(28rem, 90vw);
22
+ padding: 2rem;
23
+ border-radius: 1rem;
24
+ background: #fff;
25
+ box-shadow: 0 12px 32px rgb(15 23 42 / 12%);
26
+ }
27
+ h1 {
28
+ margin: 0 0 1rem;
29
+ font-size: 1.25rem;
30
+ }
31
+ .count {
32
+ margin: 0 0 1rem;
33
+ font-size: 1.5rem;
34
+ }
35
+ .actions {
36
+ display: flex;
37
+ gap: 0.75rem;
38
+ }
39
+ button {
40
+ padding: 0.5rem 1rem;
41
+ border-radius: 0.5rem;
42
+ border: 1px solid #d0d7de;
43
+ background: #fff;
44
+ font-size: 1rem;
45
+ cursor: pointer;
46
+ }
47
+ button:disabled {
48
+ opacity: 0.5;
49
+ cursor: not-allowed;
50
+ }
51
+ #tip {
52
+ min-height: 1.5rem;
53
+ margin: 1rem 0 0;
54
+ color: #1a7f37;
55
+ font-weight: 600;
56
+ }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <main class="card">
61
+ <h1>{{title}}</h1>
62
+ <p class="count">当前计数:<span id="count">0</span></p>
63
+ <div class="actions">
64
+ <button id="inc" type="button">+1</button>
65
+ <button id="reset" type="button" disabled>重置</button>
66
+ </div>
67
+ <p id="tip"></p>
68
+ </main>
69
+ <script type="module">
70
+ const countEl = document.querySelector('#count');
71
+ const tipEl = document.querySelector('#tip');
72
+ const incBtn = document.querySelector('#inc');
73
+ const resetBtn = document.querySelector('#reset');
74
+
75
+ let count = 0;
76
+
77
+ const render = () => {
78
+ countEl.textContent = String(count);
79
+ tipEl.textContent = count >= 3 ? '已达标' : '';
80
+ resetBtn.disabled = count === 0;
81
+ };
82
+
83
+ incBtn.addEventListener('click', () => {
84
+ count += 1;
85
+ render();
86
+ });
87
+
88
+ resetBtn.addEventListener('click', () => {
89
+ count = 0;
90
+ render();
91
+ });
92
+
93
+ render();
94
+
95
+ </script>
96
+ </body>
97
+ </html>
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ # qpc 的本地缓存(预检产物、审计快照)
4
+ .qpc/
5
+ .DS_Store
@@ -0,0 +1,41 @@
1
+ // 判题脚本必须写成 `export default defineTestHandler(...)`:判题机会把 "export default " 替换成 "const run = " 再执行。
2
+ export default defineTestHandler(async ({ page, $ }) => {
3
+ const text = async (selector) => {
4
+ const element = await page.$(selector);
5
+ if (!element) throw new Error('找不到元素 ' + selector);
6
+ return (await element.textContent()).trim();
7
+ };
8
+
9
+ // 目标按钮可能 disabled:Playwright click 会重试到 30 秒超时,需要"即使禁用也点一下"时改用合成事件。
10
+ const clickEvenIfDisabled = async (selector) => {
11
+ await page.$eval(selector, (element) => {
12
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
13
+ });
14
+ await page.waitForTimeout(150);
15
+ };
16
+
17
+ $.defineCheckPoint('初始计数为 0', 5, async () => {
18
+ const actual = await text('#count');
19
+ $.expect(actual === '0', '期望 #count 为 "0",实际为 "' + actual + '"');
20
+ return 5; // 必须 return 本检查点得分:不返回即使断言全过也记 0 分
21
+ });
22
+
23
+ $.defineCheckPoint('点击 3 次后计数为 3', 10, async () => {
24
+ for (let i = 0; i < 3; i += 1) {
25
+ // 这个按钮本就应该可用:用真实 click 并加短超时,失败时快速暴露
26
+ await page.click('#inc', { timeout: 3000 });
27
+ }
28
+ const actual = await text('#count');
29
+ $.expect(actual === '3', '期望 #count 为 "3",实际为 "' + actual + '"');
30
+ return 10;
31
+ });
32
+
33
+ $.defineCheckPoint('计数达到 3 后提示已达标', 5, async () => {
34
+ const actual = await text('#tip');
35
+ $.expect(actual === '已达标', '期望 #tip 为 "已达标",实际为 "' + actual + '"');
36
+ return 5;
37
+ });
38
+
39
+ // 需要验证"禁用状态下点击不生效"时用:await clickEvenIfDisabled('#reset');
40
+ void clickEvenIfDisabled;
41
+ });
@@ -0,0 +1,34 @@
1
+ import { defineProblemConfig } from '@challenge/problem-creator-cli';
2
+
3
+ // 出题配置。字段说明见 packages/problem-creator-cli/README.md
4
+ export default defineProblemConfig({
5
+ title: '{{title}}',
6
+ detail: [
7
+ '## 题目要求',
8
+ '',
9
+ '用一两句话写清"学生要做什么",以及判分关注的行为。',
10
+ '',
11
+ '- 点击 +1 按钮,当前计数加一',
12
+ '- 计数达到 3 时显示"已达标"',
13
+ '- 计数为 0 时重置按钮不可用',
14
+ '',
15
+ '## 提示',
16
+ '',
17
+ '题面支持 Markdown,会原样展示给学生。',
18
+ ].join('\n'),
19
+ difficulty: '{{difficulty}}',
20
+ tagIds: [{{tagIds}}],
21
+ // totalScore 留空时以检查点之和为准,填写则必须与之相等,否则审计失败(见 CFG001)。
22
+ cover: { mode: 'default' },
23
+ // 后端题型需自定义封面:cover: { mode: 'custom', generate: { bg: '#0F172A', bgTo: '#14432A', subtitle: 'GET /health' } },再跑 qpc cover --upload --write。
24
+ runtime: {
25
+ // 判题打包目录,同时决定快照键前缀(/project/...)
26
+ judgeUploadPath: '{{mountPath}}',
27
+ initCommand: '{{initCommand}}',
28
+ },
29
+ paths: {
30
+ template: 'template',
31
+ answer: 'answer',
32
+ judge: 'judge.js',
33
+ },
34
+ });
@@ -0,0 +1,74 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{title}}</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
11
+ }
12
+ body {
13
+ margin: 0;
14
+ min-height: 100vh;
15
+ display: grid;
16
+ place-items: center;
17
+ background: #f6f7f9;
18
+ color: #1f2328;
19
+ }
20
+ .card {
21
+ width: min(28rem, 90vw);
22
+ padding: 2rem;
23
+ border-radius: 1rem;
24
+ background: #fff;
25
+ box-shadow: 0 12px 32px rgb(15 23 42 / 12%);
26
+ }
27
+ h1 {
28
+ margin: 0 0 1rem;
29
+ font-size: 1.25rem;
30
+ }
31
+ .count {
32
+ margin: 0 0 1rem;
33
+ font-size: 1.5rem;
34
+ }
35
+ .actions {
36
+ display: flex;
37
+ gap: 0.75rem;
38
+ }
39
+ button {
40
+ padding: 0.5rem 1rem;
41
+ border-radius: 0.5rem;
42
+ border: 1px solid #d0d7de;
43
+ background: #fff;
44
+ font-size: 1rem;
45
+ cursor: pointer;
46
+ }
47
+ button:disabled {
48
+ opacity: 0.5;
49
+ cursor: not-allowed;
50
+ }
51
+ #tip {
52
+ min-height: 1.5rem;
53
+ margin: 1rem 0 0;
54
+ color: #1a7f37;
55
+ font-weight: 600;
56
+ }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <main class="card">
61
+ <h1>{{title}}</h1>
62
+ <p class="count">当前计数:<span id="count">0</span></p>
63
+ <div class="actions">
64
+ <button id="inc" type="button">+1</button>
65
+ <button id="reset" type="button" disabled>重置</button>
66
+ </div>
67
+ <p id="tip"></p>
68
+ </main>
69
+ <script type="module">
70
+ // TODO: 实现计数逻辑:点击 #inc 加一并更新 #count,达到 3 时 #tip 显示"已达标",为 0 时禁用 #reset。
71
+
72
+ </script>
73
+ </body>
74
+ </html>