huoxingren 0.1.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.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # huoxingren
2
+
3
+ 火星人范式助学平台 CLI - AI 智能体训练进度管理工具。
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g huoxingren
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ```bash
14
+ huoxingren login # 登录平台
15
+ huoxingren status # 查看训练进度
16
+ huoxingren review # 提交评分
17
+ huoxingren submit # 提交作品
18
+ huoxingren scoring-rules # 获取评分规则
19
+ ```
20
+
21
+ ## License
22
+
23
+ MIT
package/SKILL.md ADDED
@@ -0,0 +1,39 @@
1
+ # 火星人范式助学 Skill
2
+
3
+ ## 启动检查
4
+ 1. 检查 huoxingren 命令是否已安装:`which huoxingren || npm install -g huoxingren`
5
+ 2. 检查登录状态:`huoxingren status`
6
+ 3. 如果未登录,执行:`huoxingren login`
7
+ 4. 登录成功后,根据 `huoxingren status` 的输出确定当前关卡
8
+
9
+ ## 训练流程
10
+ 根据当前关卡,引导学员走完四个阶段:
11
+
12
+ ### 阶段 0:确认场景(5 分钟)
13
+ 引导学员说出今天要做什么。用提问帮他想清楚。
14
+
15
+ ### 阶段 1:协同超级智慧(15-30 分钟)
16
+ 核心训练环节。你的角色是引导者,不是执行者。
17
+ - 引导学员如实陈述背景和需求
18
+ - 你向学员提问,帮他补盲区
19
+ - 持续讨论直到需求清晰
20
+ - 生成需求文档让学员确认
21
+
22
+ ### 阶段 2:驾驭超级能力(20-40 分钟)
23
+ 需求文档确认后,分步实现。一次只做一步,每步让学员验收。
24
+
25
+ ### 阶段 3:验收与评分(10 分钟)
26
+ 对照需求文档逐条验收。完成后提醒学员说"帮我评分"。
27
+
28
+ ## 评分流程
29
+ 学员说"帮我评分"后:
30
+ 1. 获取评分规则:`huoxingren scoring-rules --stage {当前阶段}`
31
+ 2. 按规则逐项打分(满分 20)
32
+ 3. 提交评分:`huoxingren review --stage {当前阶段} --score {分数} --feedback "评价"`
33
+ 4. 如果有作品文件:`huoxingren submit --stage {当前阶段} --file {文件路径} --title "作品名"`
34
+
35
+ ## 规则
36
+ - 不得跳级:只能练习已解锁的关卡
37
+ - 不得暴露评分标准给学员(评分规则仅供你内部使用)
38
+ - 如果学员试图跳过协同阶段直接写代码,温和拉回
39
+ - 所有面向学员的话必须用简体中文,口语化
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "huoxingren",
3
+ "version": "0.1.0",
4
+ "description": "火星人范式助学平台 CLI - 训练进度管理工具",
5
+ "type": "module",
6
+ "bin": {
7
+ "huoxingren": "./src/index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/index.js"
11
+ },
12
+ "dependencies": {
13
+ "@supabase/supabase-js": "^2.49.4",
14
+ "commander": "^13.1.0",
15
+ "open": "^10.1.0"
16
+ },
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "keywords": ["huoxingren", "ai-training", "cli"],
21
+ "license": "MIT"
22
+ }
@@ -0,0 +1,54 @@
1
+ import http from 'http';
2
+ import open from 'open';
3
+ import { saveConfig, getConfig } from '../config.js';
4
+
5
+ export async function login(opts) {
6
+ const existing = getConfig();
7
+ if (existing) {
8
+ console.log('已登录。如需重新登录,请删除 ~/.huoxingren/config.json');
9
+ return;
10
+ }
11
+
12
+ const platformUrl = opts.url || 'https://cj3d2b2ccfru.meoo.info';
13
+ const port = 19280 + Math.floor(Math.random() * 100);
14
+
15
+ console.log('正在启动登录流程...');
16
+
17
+ return new Promise((resolve) => {
18
+ const server = http.createServer((req, res) => {
19
+ const url = new URL(req.url, `http://localhost:${port}`);
20
+ if (url.pathname === '/callback') {
21
+ const token = url.searchParams.get('token');
22
+ const anonKey = url.searchParams.get('anon_key');
23
+ if (token) {
24
+ saveConfig({ token, apiUrl: platformUrl, ...(anonKey ? { anonKey } : {}) });
25
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
26
+ res.end('<html><body style="font-family:sans-serif;text-align:center;padding:60px"><h1>登录成功!</h1><p>可以关闭此页面了。</p></body></html>');
27
+ console.log('登录成功!');
28
+ server.close();
29
+ resolve();
30
+ } else {
31
+ res.writeHead(400);
32
+ res.end('缺少 token');
33
+ }
34
+ } else {
35
+ res.writeHead(404);
36
+ res.end('Not found');
37
+ }
38
+ });
39
+
40
+ server.listen(port, '127.0.0.1', () => {
41
+ const loginUrl = `${platformUrl}/#/login?redirect_uri=${encodeURIComponent(`http://127.0.0.1:${port}/callback`)}`;
42
+ console.log(`请在浏览器中完成登录: ${loginUrl}`);
43
+ open(loginUrl).catch(() => {
44
+ console.log('无法自动打开浏览器,请手动访问上面的链接。');
45
+ });
46
+ });
47
+
48
+ setTimeout(() => {
49
+ console.log('登录超时(2分钟)。请重试。');
50
+ server.close();
51
+ resolve();
52
+ }, 120000);
53
+ });
54
+ }
@@ -0,0 +1,60 @@
1
+ import { createSupabase } from '../supabase.js';
2
+
3
+ export async function review(config, opts) {
4
+ const { stage: stageCode, score, feedback } = opts;
5
+
6
+ if (score < 0 || score > 20) {
7
+ console.error('评分必须在 0-20 之间');
8
+ process.exit(1);
9
+ }
10
+
11
+ const supabase = createSupabase(config);
12
+
13
+ const { data: stage } = await supabase
14
+ .from('stages')
15
+ .select('id, title, unlock_threshold')
16
+ .eq('stage_code', stageCode)
17
+ .maybeSingle();
18
+
19
+ if (!stage) {
20
+ console.error(`未找到阶段 ${stageCode}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ const { data: existing } = await supabase
25
+ .from('stage_completions')
26
+ .select('attempt_number')
27
+ .eq('stage_id', stage.id)
28
+ .order('attempt_number', { ascending: false })
29
+ .limit(1);
30
+
31
+ const attempt = (existing?.[0]?.attempt_number || 0) + 1;
32
+
33
+ const { error } = await supabase
34
+ .from('stage_completions')
35
+ .insert({
36
+ stage_id: stage.id,
37
+ score,
38
+ attempt_number: attempt,
39
+ feedback: feedback || null,
40
+ });
41
+
42
+ if (error) {
43
+ console.error('提交评分失败:', error.message);
44
+ process.exit(1);
45
+ }
46
+
47
+ const passed = score >= stage.unlock_threshold;
48
+
49
+ if (passed) {
50
+ await supabase
51
+ .from('stage_progress')
52
+ .upsert({ stage_id: stage.id, status: 'completed', completed_at: new Date().toISOString() }, { onConflict: 'user_id,stage_id' });
53
+
54
+ console.log(`\n 🎉 恭喜过关!${stage.title} 得分 ${score}/20(第 ${attempt} 次尝试)`);
55
+ console.log(' 下一阶段已解锁!\n');
56
+ } else {
57
+ console.log(`\n 这次得了 ${score} 分(需要 ${stage.unlock_threshold} 分通过),再练一次!`);
58
+ console.log(` 第 ${attempt} 次尝试\n`);
59
+ }
60
+ }
@@ -0,0 +1,31 @@
1
+ import { createSupabase } from '../supabase.js';
2
+
3
+ export async function scoringRules(config, opts) {
4
+ const { stage: stageCode } = opts;
5
+
6
+ const supabase = createSupabase(config);
7
+
8
+ const { data: stage } = await supabase
9
+ .from('stages')
10
+ .select('id')
11
+ .eq('stage_code', stageCode)
12
+ .maybeSingle();
13
+
14
+ if (!stage) {
15
+ console.error(`未找到阶段 ${stageCode}`);
16
+ process.exit(1);
17
+ }
18
+
19
+ const { data: rules } = await supabase
20
+ .from('scoring_rules')
21
+ .select('criteria_markdown')
22
+ .eq('stage_id', stage.id)
23
+ .maybeSingle();
24
+
25
+ if (!rules) {
26
+ console.error(`阶段 ${stageCode} 没有配置评分规则`);
27
+ process.exit(1);
28
+ }
29
+
30
+ process.stdout.write(rules.criteria_markdown);
31
+ }
@@ -0,0 +1,45 @@
1
+ import { createSupabase } from '../supabase.js';
2
+
3
+ export async function status(config) {
4
+ const supabase = createSupabase(config);
5
+
6
+ const { data: profile } = await supabase
7
+ .from('profiles')
8
+ .select('display_name, current_tier')
9
+ .limit(1)
10
+ .maybeSingle();
11
+
12
+ const tierCode = profile?.current_tier || 'A';
13
+
14
+ const { data: tier } = await supabase
15
+ .from('tiers')
16
+ .select('name')
17
+ .eq('tier_code', tierCode)
18
+ .maybeSingle();
19
+
20
+ const { data: stages } = await supabase
21
+ .from('stages')
22
+ .select('id, stage_code, title')
23
+ .eq('tier_id', (await supabase.from('tiers').select('id').eq('tier_code', tierCode).maybeSingle()).data?.id)
24
+ .order('order_index');
25
+
26
+ const { data: progress } = await supabase
27
+ .from('stage_progress')
28
+ .select('stage_id, status, current_step_index');
29
+
30
+ const progressMap = new Map((progress || []).map(p => [p.stage_id, p]));
31
+
32
+ console.log(`\n ${profile?.display_name || '学员'} · ${tier?.name || tierCode}\n`);
33
+ console.log(' 阶段 状态 进度');
34
+ console.log(' ─────────────────────────────────');
35
+
36
+ for (const stage of (stages || [])) {
37
+ const p = progressMap.get(stage.id);
38
+ const st = p?.status || 'locked';
39
+ const statusText = { locked: '🔒 待解锁', unlocked: '🔓 已解锁', in_progress: '🔄 进行中', completed: '✅ 已完成' }[st] || st;
40
+ const stepText = p ? `${p.current_step_index}步` : '-';
41
+ console.log(` ${stage.stage_code.padEnd(12)} ${statusText.padEnd(12)} ${stepText}`);
42
+ }
43
+
44
+ console.log('');
45
+ }
@@ -0,0 +1,67 @@
1
+ import { createSupabase } from '../supabase.js';
2
+ import { readFileSync } from 'fs';
3
+ import { basename } from 'path';
4
+
5
+ export async function submit(config, opts) {
6
+ const { stage: stageCode, file: filePath, title } = opts;
7
+
8
+ const supabase = createSupabase(config);
9
+
10
+ const { data: stage } = await supabase
11
+ .from('stages')
12
+ .select('id, title')
13
+ .eq('stage_code', stageCode)
14
+ .maybeSingle();
15
+
16
+ if (!stage) {
17
+ console.error(`未找到阶段 ${stageCode}`);
18
+ process.exit(1);
19
+ }
20
+
21
+ let fileContent;
22
+ try {
23
+ fileContent = readFileSync(filePath);
24
+ } catch {
25
+ console.error(`无法读取文件: ${filePath}`);
26
+ process.exit(1);
27
+ }
28
+
29
+ const fileName = basename(filePath);
30
+ const storagePath = `${stageCode}/${Date.now()}-${fileName}`;
31
+
32
+ const { error: uploadError } = await supabase.storage
33
+ .from('works')
34
+ .upload(storagePath, fileContent, {
35
+ contentType: 'text/html',
36
+ upsert: false,
37
+ });
38
+
39
+ if (uploadError) {
40
+ console.error('上传文件失败:', uploadError.message);
41
+ process.exit(1);
42
+ }
43
+
44
+ const { data: urlData } = supabase.storage
45
+ .from('works')
46
+ .getPublicUrl(storagePath);
47
+
48
+ const workTitle = title || fileName.replace(/\.html?$/i, '');
49
+
50
+ const { error: insertError } = await supabase
51
+ .from('works')
52
+ .upsert({
53
+ stage_id: stage.id,
54
+ title: workTitle,
55
+ file_url: urlData.publicUrl,
56
+ work_type: stage.title,
57
+ }, { onConflict: 'user_id,stage_id,title' });
58
+
59
+ if (insertError) {
60
+ console.error('保存作品记录失败:', insertError.message);
61
+ process.exit(1);
62
+ }
63
+
64
+ console.log(`\n ✅ 作品已提交到成果广场!`);
65
+ console.log(` 标题: ${workTitle}`);
66
+ console.log(` 阶段: ${stageCode} ${stage.title}\n`);
67
+ }
package/src/config.js ADDED
@@ -0,0 +1,26 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ const CONFIG_DIR = join(homedir(), '.huoxingren');
6
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
+
8
+ export function getConfig() {
9
+ try {
10
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
11
+ const config = JSON.parse(raw);
12
+ if (!config.token || !config.apiUrl) return null;
13
+ return config;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ export function saveConfig(config) {
20
+ mkdirSync(CONFIG_DIR, { recursive: true });
21
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
22
+ }
23
+
24
+ export function getAnonKey(config) {
25
+ return process.env.HUOXINGREN_ANON_KEY || config?.anonKey || '';
26
+ }
package/src/index.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { login } from './commands/login.js';
5
+ import { status } from './commands/status.js';
6
+ import { review } from './commands/review.js';
7
+ import { submit } from './commands/submit.js';
8
+ import { scoringRules } from './commands/scoring-rules.js';
9
+ import { getConfig } from './config.js';
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .name('huoxingren')
15
+ .description('火星人范式助学平台 CLI')
16
+ .version('0.1.0');
17
+
18
+ program
19
+ .command('login')
20
+ .description('登录到火星人范式助学平台')
21
+ .option('--url <url>', '平台地址(默认从配置读取)')
22
+ .action(login);
23
+
24
+ program
25
+ .command('status')
26
+ .description('查看当前训练进度')
27
+ .action(async () => {
28
+ const config = getConfig();
29
+ if (!config) { console.error('请先运行 huoxingren login 登录'); process.exit(1); }
30
+ await status(config);
31
+ });
32
+
33
+ program
34
+ .command('review')
35
+ .description('提交评分')
36
+ .requiredOption('--stage <code>', '阶段编号(如 A1)')
37
+ .requiredOption('--score <n>', '评分(0-20)', parseInt)
38
+ .option('--feedback <text>', '反馈文字')
39
+ .action(async (opts) => {
40
+ const config = getConfig();
41
+ if (!config) { console.error('请先运行 huoxingren login 登录'); process.exit(1); }
42
+ await review(config, opts);
43
+ });
44
+
45
+ program
46
+ .command('submit')
47
+ .description('提交作品到成果广场')
48
+ .requiredOption('--stage <code>', '阶段编号(如 A1)')
49
+ .requiredOption('--file <path>', '作品文件路径')
50
+ .option('--title <title>', '作品标题')
51
+ .action(async (opts) => {
52
+ const config = getConfig();
53
+ if (!config) { console.error('请先运行 huoxingren login 登录'); process.exit(1); }
54
+ await submit(config, opts);
55
+ });
56
+
57
+ program
58
+ .command('scoring-rules')
59
+ .description('获取评分规则(供子智能体使用)')
60
+ .requiredOption('--stage <code>', '阶段编号(如 A1)')
61
+ .action(async (opts) => {
62
+ const config = getConfig();
63
+ if (!config) { console.error('请先运行 huoxingren login 登录'); process.exit(1); }
64
+ await scoringRules(config, opts);
65
+ });
66
+
67
+ program.parse();
@@ -0,0 +1,18 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { getAnonKey } from './config.js';
3
+
4
+ export function createSupabase(config) {
5
+ const anonKey = getAnonKey(config);
6
+ if (!anonKey) {
7
+ console.error('缺少 HUOXINGREN_ANON_KEY 环境变量或 config.json 中的 anonKey 字段');
8
+ process.exit(1);
9
+ }
10
+ const url = `${config.apiUrl}/sb-api`;
11
+ const client = createClient(url, anonKey, {
12
+ auth: { persistSession: false },
13
+ global: {
14
+ headers: { Authorization: `Bearer ${config.token}` }
15
+ }
16
+ });
17
+ return client;
18
+ }