Skills
2929 foundAgent Skills are multi-file prompts that give AI agents specialized capabilities. They include instructions, configurations, and supporting files that can be used with Claude, Cursor, Windsurf, and other AI coding assistants.
Reframe a player's current situation to reveal new meaning, goals, roles, or playstyles without changing the underlying mechanics. Use when diagnosing stagna...
--- name: game-design-player-perspective-reframe description: Reframe a player's current situation to reveal new meaning, goals, roles, or playstyles without changing the underlying mechanics. Use when diagnosing stagnation, boredom, or mid/late-game disengagement; when designing re-engagement prompts, adaptive guidance, or dynamic missions; or when a player is technically able to continue but no longer sees the current state as interesting, valuable, or purposeful. --- # Game Design Player Perspective Reframe Reframe a player's current situation so the same game state can be interpreted through a more motivating lens. Use this skill when the player is not blocked by a fundamentally broken system, but by a stale interpretation of what their situation means or what kind of play is currently available to them. ## Core principle Sometimes the problem is not lack of content, but lack of meaning. A player can have options available and still feel stuck because they are reading the current state through an exhausted frame: "I cannot grow," "I am behind," "nothing is happening," or "this part is just waiting." Reframing changes the interpretation of the state so a new kind of goal, role, or challenge becomes visible. ## What to produce Generate: 1. **Current state summary** - what the player is doing, wanting, and feeling 2. **Stagnation diagnosis** - why the current frame is no longer working 3. **Reframe options** - alternative ways to interpret the current state 4. **Chosen reframe** - the strongest new lens 5. **Action hook** - immediate next objective or prompt 6. **Expected effect** - why the reframe may restore interest, agency, or momentum 7. **Use-case judgment** - whether reframing is actually the right intervention, or whether the underlying system instead needs fixing ## Process ### 1. Define the stuck state Clarify: - what the player is trying to do - what they believe is the problem - what the system state actually looks like - what kind of disengagement is happening: boredom, frustration, aimlessness, repetition, self-comparison fatigue, etc. Write: - **Player state** - **Current goal** - **Why the current frame is failing** ### 2. Decide whether reframing is appropriate at all Before generating reframes, check whether the problem is truly interpretive rather than structural. Reframing is appropriate when: - the player has meaningful options, but does not currently value or notice them - the underlying systems are basically sound, but the player's current lens is exhausted - the game can support alternate self-directed goals without pretending the state is healthier than it is - the intervention is meant to extend or redirect engagement, not conceal a broken loop Reframing is not the right primary move when: - the system is actually opaque, unfair, or under-rewarding - the player lacks real agency or feasible next steps - the economy is over-constrained and the reframe would just romanticize waiting - frustration is caused by balance, UX, matchmaking, or monetization abuse If the issue is mostly structural, say so clearly and treat any reframe as secondary at best. ### 3. Diagnose the dominant stagnation pattern Common patterns: - **growth lock** - player only values expansion and cannot see value in consolidation - **efficiency fatigue** - player is optimizing mechanically but no longer feels purpose - **goal vacuum** - no compelling next objective is visible - **identity exhaustion** - player has overidentified with one role or playstyle - **failure fixation** - player reads current state only as a deficit or loss - **content blindness** - systems are present but the player does not recognize them as meaningful play ### 4. Choose a reframe strategy Use one or combine several: #### Role reframe Shift who the player is right now. Examples: - builder -> optimizer - collector -> curator - attacker -> steward - grinder -> planner #### Goal reframe Shift what success means. Examples: - expansion -> refinement - speed -> elegance - raw power -> consistency - completion -> experimentation #### Constraint reframe Turn a limitation into a challenge premise. Examples: - "What can you achieve with only your current tools?" - "Can you solve this with one district / one deck / one weapon class?" #### System reframe Reveal another layer of meaning already present in the same mechanics. Examples: - "This is not just waiting; this is production planning." - "This is not a content gap; it is a logistics puzzle." #### Narrative reframe Wrap the current state in story meaning. Examples: - recovery phase - rebuilding chapter - proving-ground moment - specialist mission #### Social reframe Redefine the current state through comparison, contribution, or recognition. Examples: - show off efficiency - mentor others - attempt a community challenge - compare style rather than speed ### 5. Generate multiple plausible reframes Produce at least three candidate reframes before choosing one. Each candidate should include: - new interpretation - why it fits the current state - what kind of player it is most likely to help - risk of backfiring ### 6. Select the best reframe Pick the reframe most likely to: - restore agency - make the current state feel meaningful - create an immediate next step - fit the player's likely values - avoid lying about a broken system Important: do not use reframing to excuse an actually broken or abusive loop. If the system is fundamentally busted, say so. ### 7. Attach an action hook The reframe must point to a concrete next move. Examples: - optimize output using only current buildings - redesign one district around beauty instead of income - complete a self-imposed low-resource challenge - treat the next three sessions as a scouting-and-planning phase - focus on one underused system and master it Without an action hook, the reframe stays abstract and weak. ### 8. State the expected effect The expected effect should be modest and believable. Good targets: - renewed curiosity - restored short-term agency - lower self-defeating frustration - better recognition of alternate goals already present in the system - a temporary bridge from stale play to fresher play Bad targets: - masking a broken progression wall - making players accept exploitative friction - pretending a starved content phase is secretly rich ### 9. State the use-case judgment Conclude with a blunt judgment: - **Strong fit for reframing** - **Partial fit; system fixes matter more** - **Weak fit; this is mostly a structural problem** Say why. Explain what the reframe is trying to change: - restore curiosity - reduce frustration by changing success criteria - open a new playstyle identity - create a short-term challenge layer - transform waiting into anticipation or planning ## Response structure ### Current State Summary - ... ### Stagnation Diagnosis - ... ### Reframe Options 1. ... 2. ... 3. ... ### Chosen Reframe - ... ### Action Hook - ... ### Expected Effect - ... ### Use-Case Judgment - ... ## Fast mode Use this quick pass when speed matters: - what is the player currently trying to do? - is the problem interpretive or structural? - why does the current frame feel dead? - what other role, goal, or lens could fit the same state? - what should the player do immediately under that new frame? ## Working principle A good reframe does not pretend the player's situation is different. It makes a different and more useful truth visible inside the same situation.
Infer a player's underlying values and motivational priorities from behavior, then translate those into design implications. Use when designing personalizati...
--- name: game-design-player-values-mapper description: Infer a player's underlying values and motivational priorities from behavior, then translate those into design implications. Use when designing personalization, segmentation, dynamic guidance, live-ops targeting, adaptive missions, re-engagement strategies, or feature prioritization; when behavior suggests that what players actually care about differs from what the design assumes; or when a team needs a behavior-first player profile rather than a demographic or archetype-only model. --- # Game Design Player Values Mapper Map observed player behavior to likely underlying value priorities, then use that map to infer what kinds of goals, rewards, content, or framing are most likely to resonate. Use this skill when the team needs to understand not just what players do, but what those choices imply about what they care about. ## Core principle Behavior is not random. It is preference made visible. Players reveal their values through repetition, avoidance, investment, and attention. The goal is not to assign a rigid personality label, but to infer the motivational structure most likely driving current behavior and use that to improve design alignment. ## What to produce Generate: 1. **Observed behavior summary** - what the player consistently does, ignores, and invests in 2. **Value map** - likely dominant, secondary, and weak values 3. **Confidence notes** - how strong or ambiguous each inference is 4. **Tensions or contradictions** - where behavior suggests mixed motives or blocked values 5. **Design implications** - what systems, content, messaging, goals, or monetization surfaces are likely aligned or misaligned 6. **Segment hypothesis** - what kind of player pattern this most resembles in practical design terms 7. **Recommendations** - what to emphasize, reframe, personalize, or stop pushing ## Value framework Map behavior to these value dimensions: - **Efficiency / Optimization** - **Progression / Growth** - **Aesthetics / Expression** - **Collection / Completion** - **Social Recognition / Status** - **Experimentation / Discovery** - **Narrative / Meaning** You may add a clearly justified extra value if the case demands it, but do not bloat the framework casually. ## Process ### 1. Gather behavior signals List concrete observed behaviors. Possible sources: - build patterns - resource spending - session frequency and duration - event participation - feature engagement - purchase behavior - social behavior - what the player returns to repeatedly - what the player ignores despite obvious rewards Write: - **Repeated behaviors** - **Avoided behaviors** - **Investment patterns** ### 2. Map behaviors to likely value signals Translate behavior into value hypotheses. Examples: - min-maxing production chains -> Efficiency / Optimization - constant upgrading and rushing unlocks -> Progression / Growth - decorating, styling, curating loadouts -> Aesthetics / Expression - chasing every item or badge -> Collection / Completion - caring about ranks, cosmetics, visibility -> Social Recognition / Status - trying odd builds or niche tools -> Experimentation / Discovery - following lore, theme, faction identity, story arcs -> Narrative / Meaning Important: many behaviors can map to more than one value. Do not overclaim certainty. ### 3. Weight the value profile Do not force fake precision. The goal is a useful profile, not pseudo-scientific certainty. Assign rough weight levels such as: - High - Medium - Low Or if needed: - Dominant - Secondary - Weak - Absent Also note confidence: - high confidence - medium confidence - low confidence Use this format: | Value | Weight | Confidence | Evidence | |---|---|---|---| | ... | ... | ... | ... | ### 4. Detect tensions and blocked values Look for contradictions. Examples: - optimization-driven player engaging with decoration only because progression forces it - status-seeking player avoiding competition because the failure cost feels humiliating - progression-oriented player not spending because they distrust the offer structure - discovery-oriented player repeating safe loops because experimentation is too punished Ask: - is this a real mixed-value profile? - or is one value being blocked by system design? ### 5. Infer likely design alignment Answer: - what currently motivates this player most? - what kinds of content or objectives will likely land well? - what incentives are probably weak for this player? - where is the game asking for a value the player does not strongly hold? - what part of the experience is likely causing silent disengagement? - what messaging, reward framing, or mission framing is most likely to resonate? ### 6. Form a practical segment hypothesis Translate the value map into a practical design-facing player pattern. Examples: - efficiency-first optimizer - completionist collector with moderate status drive - expressive builder with weak progression urgency - growth-focused grinder with low experimentation tolerance - discovery-oriented tinkerer blocked by punishment This is not meant to replace deeper persona work. It is a compact operational summary that helps teams act. ### 7. Recommend design actions Translate the value map into actions such as: - personalize mission framing - surface a different kind of goal - target events/offers more intelligently - reduce pressure toward misaligned systems - give better tools to the dominant value type - redesign progression framing for the current segment - change how rewards are explained, not just what rewards are given - stop over-serving a secondary value while neglecting the dominant one ## Response structure ### Observed Behavior Summary - ... ### Player Value Map | Value | Weight | Confidence | Evidence | |---|---|---|---| | ... | ... | ... | ... | ### Dominant Values - ... ### Secondary Values - ... ### Tensions / Contradictions - ... ### Segment Hypothesis - ... ### Design Implications - ... ### Recommendations 1. ... 2. ... 3. ... ## Fast mode Use this quick pass when speed matters: - what does the player repeatedly choose? - what do they ignore? - what does that imply they value? - what is the strongest mismatch between the player's values and the game's current asks? - what practical segment hypothesis best describes this player? - what should the design emphasize or stop emphasizing for this player? ## Working principle A player rarely says their values directly. They leak them constantly through what they pursue, what they skip, and what they are willing to suffer for.
Audit a game, feature flow, economy path, onboarding journey, progression chain, or live-ops loop for friction quality and friction accumulation. Use when di...
--- name: game-design-friction-journey-audit description: Audit a game, feature flow, economy path, onboarding journey, progression chain, or live-ops loop for friction quality and friction accumulation. Use when diagnosing where players stall, disengage, churn, or feel overloaded; when distinguishing productive challenge from harmful friction; or when evaluating whether constraints, waiting, confusion, resource pressure, or multi-step dependencies are creating strategy, tension, frustration, or deadlock. --- # Game Design Friction Journey Audit Audit a design by mapping where friction appears across a player journey, what kind of friction it is, how it accumulates, and where useful challenge mutates into harmful drag. Use this skill when a feature feels sticky in the wrong way, when progression seems to slow down for reasons players cannot articulate clearly, or when you need to separate meaningful challenge from accidental obstruction. ## Core principle Not all friction is bad. Some friction creates commitment, decision-making, anticipation, and mastery. Other friction creates confusion, paralysis, resentment, or churn. The job is not to remove all resistance. The job is to identify which resistance is doing design work and which is merely getting in the player's way. ## What to produce Generate: 1. **Audit target** - what journey, loop, or feature is being reviewed 2. **Journey breakdown** - the major steps in player progression through the target flow 3. **Friction map** - where friction appears, what kind it is, and what causes it 4. **Accumulation analysis** - where multiple frictions stack into exhaustion or deadlock 5. **Diagnosis** - where the design shifts from meaningful challenge to harmful blockage 6. **Recommendations** - what to preserve, reduce, surface, reorder, or remove ## Process ### 1. Define the journey being audited Clarify: - what system or flow is under review - what kind of player it applies to - what stage of play it belongs to: FTUE, early game, mid-game, elder game, event loop, monetization path, social loop, etc. - what desired player behavior the flow is supposed to support Write: - **Audit target** - **Expected player goal** - **Player context** ### 2. Break the journey into steps Map the journey as a sequence of player-facing steps. For each step, identify: - player action - player decision - requirement or dependency - feedback or reward - what unlocks the next step Keep steps coarse enough to be readable but concrete enough to locate friction. ### 3. Identify friction at each step For each step, ask: - what slows progress? - what blocks progress? - what creates uncertainty? - what consumes time, attention, or resources? - what forces tradeoffs or commitment? Possible friction sources: - resource scarcity - dependency chains - waiting and timers - unclear affordances or goals - UI or information opacity - cognitive overload - skill challenge - social coordination burden - random variance - harsh penalty or recovery cost - monetization pressure ### 4. Classify the friction Classify each friction point as one of these: #### Productive friction Supports: - decision-making - planning - anticipation - mastery - commitment - strategic tradeoff - emotional tension that feels fair and legible #### Harmful friction Produces: - confusion - dead time without meaning - arbitrary blocking - unreadable requirements - overloaded task chains - repeated admin work - punishment without learning - progress paralysis #### Mixed friction Useful in principle, but currently too strong, too opaque, too stacked, or too poorly timed. Do not treat this as binary if it is not. Many systems are good ideas implemented at the wrong intensity. ### 5. Assess intensity and visibility For each friction point, rate: - **Intensity** - low / medium / high - **Visibility** - obvious / partially hidden / opaque - **Fairness feel** - fair / borderline / unfair-feeling A friction can be mild but still dangerous if it is hidden. It can also be intense but acceptable if the player clearly understands it and sees why it exists. ### 6. Analyze friction accumulation Look for stack effects. Ask: - where do several medium frictions compound into a high-friction moment? - where are players forced to satisfy too many constraints at once? - where does the flow ask for too much memory, too much waiting, or too many parallel tasks? - where do repeated harmful frictions appear without enough reward, clarity, or release? Common accumulation patterns: - multiple resources plus timer plus low clarity - complex chain plus weak feedback plus low inventory space - repeated losses plus long recovery plus weak learning signal - social obligation plus schedule pressure plus poor coordination tools ### 7. Find the breakpoints Identify: - where challenge turns into drag - where strategy turns into opacity - where anticipation turns into dead time - where difficulty turns into helplessness - where a healthy loop turns into churn risk These are the key design breakpoints. ### 8. Diagnose the role of friction in the design Answer: - which friction points are core to the fantasy or mastery arc? - which friction points only exist because of weak clarity, weak UX, poor pacing, or over-constrained economy? - what friction is essential and should be protected? - what friction is currently doing accidental damage? ### 9. Recommend design changes For each major friction issue, specify: - **Issue** - **Why it hurts** - **Keep / reduce / remove / surface / reorder / soften** - **Expected effect** Typical interventions: - surface hidden requirements - reduce simultaneous constraints - improve feedback and goal clarity - shorten dead-time without removing commitment - preserve meaningful tradeoffs while removing admin burden - stagger dependencies instead of stacking them all at once ## Response structure ### Audit Target - ... ### Journey Breakdown 1. ... 2. ... 3. ... ### Friction Map | Step | Friction Point | Type | Cause | Intensity | Visibility | Fairness Feel | |---|---|---|---|---|---|---| | ... | ... | ... | ... | ... | ... | ... | ### Accumulation Analysis - ... ### Breakpoints - ... ### Diagnosis - ... ### Recommendations 1. ... 2. ... 3. ... ## Fast mode Use this quick pass when speed matters: - where does the player slow down or stop? - is the friction creating strategy or confusion? - is it fair and legible? - what other frictions are stacking nearby? - what should be preserved, softened, surfaced, or removed? ## Working principle Good friction gives the player something meaningful to push against. Bad friction makes the player wonder why they are pushing at all.
以《周易》本经原著为底,系统收集并逐一拆解市面上几乎所有可获取的同类 divination agents / skills / 程序, 汲取百家之长,取其精华,去其糟粕,最终打磨成这一套更准确、更完整、更好用的周易系统。它把同类产品里 最成熟的交互、百科式组织、起卦体验和规则呈现方式整合进来,同时去掉卦序错误、原...
---
name: zhouyi-benjing-oracle
clawhub-slug: zhouyi-benjing-oracle
description: |
以《周易》本经原著为底,系统收集并逐一拆解市面上几乎所有可获取的同类 divination agents / skills / 程序,
汲取百家之长,取其精华,去其糟粕,最终打磨成这一套更准确、更完整、更好用的周易系统。它把同类产品里
最成熟的交互、百科式组织、起卦体验和规则呈现方式整合进来,同时去掉卦序错误、原文截断、白话混原文、
未校验命理乱炖和伪精确排盘,让《周易》回到原著,也把产品做到更像市面上值得长期留下来的那一个。
Built on the original Zhouyi text, this skill was forged by systematically collecting and dissecting virtually
every comparable divination agent, skill, and app we could access, absorbing the best ideas across the field
while stripping away the noise. We kept the strongest UX, encyclopedia structure, casting flow, and rule
presentation, and removed the usual flaws: wrong hexagram mappings, truncated canon, paraphrase mixed into
scripture, unverified metaphysics mashups, and fake precision. The result is a cleaner, stronger, and more
enduring I Ching product.
license: MIT-0
compatibility:
platforms:
- claude-code
- claude-ai
- api
metadata:
author: pineapple
version: "1.0.0"
tags: ["zhouyi", "yijing", "iching", "周易", "易经", "六十四卦", "占卜", "卦辞", "爻辞"]
openclaw:
emoji: "☯"
skillKey: "zhouyi-benjing-oracle"
requires:
bins:
- node
---
# 周易本经占筮
这是一个以《周易》本经为底座的技能包。它包含三部分能力:
1. `起卦`:三钱或蓍草起卦,按七种变爻规则取辞。
2. `查卦`:查六十四卦卦辞、爻辞、用九、用六。
3. `路由`:给出术数百科式总览,但只把周易本经模块当作已校验核心。
## 何时使用
当用户出现以下需求时,优先使用本技能:
- “帮我起一卦”
- “用周易看一下这件事”
- “查乾卦/屯卦/某句卦辞”
- “这个卦该看哪条爻辞”
- “周易和六爻/梅花/八字有什么区别”
- “我想看这个系统怎么用”
以下需求不要冒充已实现高精度:
- 八字排盘
- 奇门遁甲排盘
- 紫微斗数排盘
- 六爻纳甲断卦
这些内容目前只在 `术数百科` 中作为资料要求和边界说明存在。
## 默认工作流
### 1. 现场起卦
优先调用:
```bash
node scripts/zhouyi_cli.js cast --question "我是否该推进这次合作" --method coin --json
```
可选方法:
- `coin`:三钱法,6/7/8/9 概率为 1/8、3/8、3/8、1/8
- `yarrow`:蓍草概率模拟,6/7/8/9 概率为 1/16、5/16、7/16、3/16
如果需要复现结果,可带种子:
```bash
node scripts/zhouyi_cli.js cast --question "测试" --method coin --seed demo --json
```
### 2. 查某一卦
```bash
node scripts/zhouyi_cli.js lookup --name 乾 --json
node scripts/zhouyi_cli.js lookup --number 3 --json
```
### 3. 按关键词搜原文
```bash
node scripts/zhouyi_cli.js search --query "十年乃字" --json
node scripts/zhouyi_cli.js search --query "利涉大川" --json
```
### 4. 看术数百科路由
```bash
node scripts/zhouyi_cli.js catalog --json
node scripts/zhouyi_cli.js catalog --grade S --query 周易 --json
```
### 5. 打开内置网页
如果用户想要直接操作本地网页,打开根目录的 `index.html` 即可。网页包含:
- 周易本经占筮界面
- 六十四卦本经库
- 术数百科导航
- 近占记录
## 解读规则
调用 `cast` 后,按下列规则取辞:
1. 六爻不变:用本卦卦辞。
2. 一爻变:用该动爻爻辞。
3. 二爻变:用两个动爻爻辞,以上爻为主。
4. 三爻变:用本卦卦辞与变卦卦辞。
5. 四爻变:用两个静爻爻辞,以下爻为主。
6. 五爻变:用变卦中唯一静爻所对应的爻辞。
7. 六爻皆变:乾用用九,坤用用六,其余用变卦卦辞。
## 输出原则
1. 先交代本次取辞规则。
2. 再引用本经原文。
3. 最后给白话解释。
4. 不把解释说成确定命令。
5. 不用未校验体系污染周易本经结论。
## 参考资源
- 产品与验证说明:`references/README.md`
- 小白使用说明:`references/user-guide-zh.md`
- 本经底本:`references/zhouyi-benjing-source.txt`
## 维护命令
重建本经数据:
```bash
python3 scripts/build_zhouyi_data.py
```
运行校验:
```bash
node tests/verify_zhouyi_system.js
node tests/verify_system_catalog.js
node tests/verify_cli.js
```
FILE:_meta.json
{
"slug": "zhouyi-benjing-oracle",
"version": "1.0.0"
}
FILE:index.html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>周易本经占筮</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main class="shell">
<section class="oracle">
<header class="masthead">
<div>
<p class="eyebrow">周易本经 · 六爻取辞 · 易问</p>
<h1>周易本经占筮</h1>
</div>
<div class="bagua-ring" aria-hidden="true">
<span>☰</span><span>☱</span><span>☲</span><span>☳</span>
<span>☴</span><span>☵</span><span>☶</span><span>☷</span>
</div>
</header>
<section class="casting-panel">
<label class="question-label" for="question">当下之问</label>
<textarea
id="question"
rows="4"
placeholder="例如:我是否应该在这个月推进新的合作?"
></textarea>
<div class="controls">
<div class="mode-switch" role="group" aria-label="起卦方式">
<button class="mode-button active" type="button" data-method="coin">三钱</button>
<button class="mode-button" type="button" data-method="yarrow">蓍草</button>
</div>
<button class="cast-button" id="castButton" type="button">
<span class="button-icon" aria-hidden="true">◎</span>
起卦
</button>
</div>
</section>
</section>
<section class="atlas-section">
<div class="section-heading">
<div>
<p class="eyebrow">全体系导航</p>
<h2>术数百科</h2>
</div>
<div class="atlas-search">
<input id="catalogSearch" type="search" placeholder="搜索体系、用途或资料要求" />
</div>
</div>
<div class="grade-filter" id="catalogFilters" role="group" aria-label="精度筛选">
<button class="filter-button active" type="button" data-grade="all">全部</button>
<button class="filter-button" type="button" data-grade="S">S 级</button>
<button class="filter-button" type="button" data-grade="B">B 级</button>
<button class="filter-button" type="button" data-grade="C">C 级</button>
</div>
<div id="catalogGrid" class="catalog-grid"></div>
</section>
<section class="library-section">
<div class="section-heading">
<div>
<p class="eyebrow">六十四卦</p>
<h2>本经卦库</h2>
</div>
<div class="atlas-search">
<input id="hexagramSearch" type="search" placeholder="搜索卦名、卦辞或爻辞" />
</div>
</div>
<div id="hexagramLibrary" class="hexagram-library"></div>
</section>
<section class="result-layout" id="resultLayout" hidden>
<section class="hexagram-stage" aria-live="polite">
<div class="hexagram-visuals">
<div class="hex-block">
<p class="block-label">本卦</p>
<div class="hexagram" id="primaryHexagram"></div>
<h2 id="primaryTitle">-</h2>
<p id="primaryTrigrams">-</p>
</div>
<div class="change-arrow" id="changeArrow">→</div>
<div class="hex-block secondary" id="changedBlock">
<p class="block-label">变卦</p>
<div class="hexagram" id="changedHexagram"></div>
<h2 id="changedTitle">-</h2>
<p id="changedTrigrams">-</p>
</div>
</div>
</section>
<aside class="reading-panel">
<div class="reading-header">
<p class="eyebrow">本经取辞</p>
<button class="icon-button" id="copyButton" type="button" title="复制解读">
⧉
</button>
</div>
<div id="readingText" class="reading-text"></div>
</aside>
</section>
<section class="detail-grid" id="detailGrid" hidden>
<article class="detail-panel">
<h3>六爻</h3>
<div id="lineList" class="line-list"></div>
</article>
<article class="detail-panel">
<h3>本经线索</h3>
<div id="symbolList" class="symbol-list"></div>
</article>
<article class="detail-panel journal-panel">
<div class="panel-title-row">
<h3>近占</h3>
<button class="icon-button" id="clearJournalButton" type="button" title="清空记录">
×
</button>
</div>
<div id="journalList" class="journal-list"></div>
</article>
</section>
</main>
<script src="./data/zhouyi-benjing.js"></script>
<script src="./data/system-catalog.js"></script>
<script src="./app.js"></script>
</body>
</html>
FILE:styles.css
:root {
color-scheme: light;
--ink: #171614;
--muted: #67605a;
--paper: #f8f5ef;
--paper-deep: #ece2d2;
--line: #d8c8b1;
--jade: #1f6f63;
--jade-soft: #dceae5;
--cinnabar: #b5432f;
--gold: #b88738;
--night: #202428;
--white: #fffdfa;
--shadow: 0 22px 70px rgba(49, 39, 25, 0.14);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
background:
linear-gradient(90deg, rgba(31, 111, 99, 0.08) 1px, transparent 1px),
linear-gradient(0deg, rgba(184, 135, 56, 0.08) 1px, transparent 1px),
var(--paper);
background-size: 42px 42px;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
"PingFang SC", "Microsoft YaHei", sans-serif;
}
button,
textarea,
input {
font: inherit;
}
button {
cursor: pointer;
}
.shell {
width: min(1180px, calc(100vw - 32px));
margin: 0 auto;
padding: 28px 0 44px;
}
.oracle {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(320px, 1.1fr);
gap: 28px;
align-items: stretch;
min-height: 42vh;
}
.masthead {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
min-height: 360px;
padding: 32px;
color: var(--white);
background:
linear-gradient(rgba(23, 22, 20, 0.45), rgba(23, 22, 20, 0.3)),
radial-gradient(circle at 74% 30%, rgba(184, 135, 56, 0.62), transparent 34%),
linear-gradient(135deg, #1f6f63 0%, #273136 58%, #612d25 100%);
border-radius: 8px;
box-shadow: var(--shadow);
}
.masthead::before {
content: "";
position: absolute;
inset: 18px;
border: 1px solid rgba(255, 253, 250, 0.24);
pointer-events: none;
}
.eyebrow {
margin: 0 0 10px;
color: inherit;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0;
opacity: 0.72;
}
h1 {
position: relative;
z-index: 1;
margin: 0;
font-size: clamp(3rem, 8vw, 6.8rem);
line-height: 0.92;
letter-spacing: 0;
}
.bagua-ring {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: repeat(4, minmax(42px, 1fr));
gap: 10px;
width: min(100%, 360px);
color: rgba(255, 253, 250, 0.9);
}
.bagua-ring span {
display: grid;
place-items: center;
aspect-ratio: 1;
border: 1px solid rgba(255, 253, 250, 0.25);
background: rgba(255, 253, 250, 0.08);
font-size: clamp(1.8rem, 4vw, 3rem);
}
.casting-panel {
display: flex;
flex-direction: column;
justify-content: center;
gap: 16px;
padding: 30px;
background: rgba(255, 253, 250, 0.82);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
}
.question-label {
color: var(--muted);
font-weight: 750;
}
textarea {
width: 100%;
min-height: 160px;
resize: vertical;
padding: 18px;
color: var(--ink);
background: var(--white);
border: 1px solid var(--line);
border-radius: 8px;
outline: none;
line-height: 1.7;
}
textarea:focus {
border-color: var(--jade);
box-shadow: 0 0 0 4px rgba(31, 111, 99, 0.12);
}
input[type="search"] {
width: 100%;
min-height: 44px;
padding: 0 14px;
color: var(--ink);
background: var(--white);
border: 1px solid var(--line);
border-radius: 8px;
outline: none;
}
input[type="search"]:focus {
border-color: var(--jade);
box-shadow: 0 0 0 4px rgba(31, 111, 99, 0.12);
}
.controls {
display: flex;
gap: 14px;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
.mode-switch {
display: inline-grid;
grid-template-columns: repeat(2, minmax(78px, 1fr));
padding: 4px;
background: var(--paper-deep);
border: 1px solid var(--line);
border-radius: 8px;
}
.mode-button {
min-height: 42px;
padding: 0 16px;
color: var(--muted);
background: transparent;
border: 0;
border-radius: 6px;
font-weight: 750;
}
.mode-button.active {
color: var(--white);
background: var(--jade);
}
.cast-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
min-width: 148px;
min-height: 52px;
padding: 0 22px;
color: var(--white);
background: var(--cinnabar);
border: 0;
border-radius: 8px;
font-weight: 850;
box-shadow: 0 12px 24px rgba(181, 67, 47, 0.22);
}
.button-icon {
font-size: 1.35rem;
}
.result-layout {
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(340px, 0.95fr);
gap: 24px;
margin-top: 26px;
}
.atlas-section,
.library-section {
margin-top: 26px;
padding: 24px;
background: rgba(255, 253, 250, 0.88);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
}
.section-heading {
display: flex;
align-items: end;
justify-content: space-between;
gap: 18px;
margin-bottom: 16px;
}
.section-heading h2 {
margin: 0;
font-size: clamp(1.6rem, 3vw, 2.4rem);
}
.atlas-search {
width: min(100%, 360px);
}
.grade-filter {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.filter-button {
min-height: 38px;
padding: 0 14px;
color: var(--muted);
background: var(--paper-deep);
border: 1px solid var(--line);
border-radius: 8px;
font-weight: 800;
}
.filter-button.active {
color: var(--white);
background: var(--jade);
border-color: var(--jade);
}
.catalog-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.system-card,
.hex-card {
min-height: 100%;
padding: 16px;
background: rgba(255, 253, 250, 0.82);
border: 1px solid rgba(216, 200, 177, 0.82);
border-radius: 8px;
}
.system-card header,
.hex-card header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 12px;
}
.system-card h3,
.hex-card h3 {
margin: 0;
font-size: 1.05rem;
}
.system-card p,
.hex-card p {
margin: 8px 0 0;
color: var(--muted);
line-height: 1.62;
font-size: 0.94rem;
}
.grade-badge,
.status-badge {
display: inline-flex;
align-items: center;
min-height: 26px;
padding: 0 8px;
color: var(--white);
background: var(--night);
border-radius: 6px;
font-size: 0.76rem;
font-weight: 850;
white-space: nowrap;
}
.grade-s {
background: var(--jade);
}
.grade-b {
background: var(--gold);
}
.grade-c {
background: var(--muted);
}
.status-badge {
margin-top: 10px;
color: var(--ink);
background: var(--paper-deep);
}
.tag-list {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 12px;
}
.tag-list span {
padding: 4px 7px;
color: var(--jade);
background: var(--jade-soft);
border-radius: 6px;
font-size: 0.78rem;
font-weight: 760;
}
.hexagram-library {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
max-height: 520px;
overflow: auto;
padding-right: 4px;
}
.hex-card {
min-height: 170px;
}
.hex-card .source-line {
color: var(--ink);
font-family: "Songti SC", "STSong", "Noto Serif CJK SC", serif;
}
.hexagram-stage,
.reading-panel,
.detail-panel {
background: rgba(255, 253, 250, 0.88);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
}
.hexagram-stage {
padding: 26px;
}
.hexagram-visuals {
display: grid;
grid-template-columns: minmax(220px, 1fr) 48px minmax(220px, 1fr);
gap: 16px;
align-items: center;
}
.hex-block {
min-height: 430px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
border: 1px solid rgba(216, 200, 177, 0.72);
background: linear-gradient(180deg, #fffdfa, #f4ecdf);
border-radius: 8px;
}
.hex-block.secondary {
background: linear-gradient(180deg, #fbfaf6, #e9f1ed);
}
.block-label {
margin: 0 0 18px;
color: var(--muted);
font-size: 0.8rem;
font-weight: 800;
}
.hexagram {
display: flex;
flex-direction: column;
justify-content: center;
gap: 12px;
width: min(100%, 240px);
min-height: 248px;
}
.yao {
position: relative;
display: grid;
grid-template-columns: 1fr;
align-items: center;
min-height: 28px;
}
.yao::before,
.yao::after {
content: "";
height: 14px;
background: var(--night);
border-radius: 2px;
}
.yao.yin {
grid-template-columns: 1fr 34px 1fr;
}
.yao.yin::before,
.yao.yin::after {
display: block;
}
.yao.yin::before {
grid-column: 1;
}
.yao.yin::after {
grid-column: 3;
}
.yao.yang::after {
display: none;
}
.yao.moving::before,
.yao.moving::after {
background: var(--cinnabar);
}
.yao.moving::marker {
color: var(--cinnabar);
}
.yao .move-dot {
position: absolute;
right: -24px;
width: 10px;
height: 10px;
border-radius: 999px;
background: var(--gold);
box-shadow: 0 0 0 5px rgba(184, 135, 56, 0.18);
}
.hex-block h2 {
margin: 18px 0 8px;
text-align: center;
font-size: clamp(1.35rem, 3vw, 2rem);
}
.hex-block p:last-child {
margin: 0;
color: var(--muted);
text-align: center;
line-height: 1.6;
}
.change-arrow {
display: grid;
place-items: center;
width: 48px;
aspect-ratio: 1;
color: var(--jade);
border: 1px solid var(--line);
border-radius: 50%;
background: var(--jade-soft);
font-size: 1.7rem;
font-weight: 900;
}
.reading-panel {
padding: 24px;
}
.reading-header,
.panel-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.icon-button {
display: inline-grid;
place-items: center;
width: 38px;
height: 38px;
color: var(--ink);
background: var(--white);
border: 1px solid var(--line);
border-radius: 8px;
font-size: 1.2rem;
}
.reading-text {
display: grid;
gap: 16px;
margin-top: 12px;
}
.reading-text section {
padding-top: 14px;
border-top: 1px solid rgba(216, 200, 177, 0.7);
}
.reading-text h3,
.detail-panel h3 {
margin: 0 0 10px;
font-size: 1rem;
}
.reading-text p {
margin: 0;
color: var(--muted);
line-height: 1.8;
}
.reading-text blockquote {
margin: 0;
padding: 12px 14px;
color: var(--ink);
background: #fff8ea;
border-left: 3px solid var(--gold);
line-height: 1.8;
}
.reading-text blockquote strong {
display: block;
margin-bottom: 4px;
color: var(--cinnabar);
}
.reading-text blockquote.primary-source {
background: var(--jade-soft);
border-left-color: var(--jade);
}
.source-stack {
display: grid;
gap: 10px;
margin-top: 10px;
}
.rule-badge {
display: inline-flex;
align-items: center;
min-height: 26px;
margin-right: 8px;
padding: 0 9px;
color: var(--white);
background: var(--jade);
border-radius: 6px;
font-size: 0.82rem;
font-weight: 800;
}
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr 0.9fr;
gap: 18px;
margin-top: 18px;
}
.detail-panel {
min-height: 220px;
padding: 22px;
}
.line-list,
.symbol-list,
.journal-list {
display: grid;
gap: 10px;
}
.line-item,
.symbol-item,
.journal-item {
padding: 12px 0;
color: inherit;
background: transparent;
border: 0;
border-bottom: 1px solid rgba(216, 200, 177, 0.78);
border-radius: 0;
}
.line-item strong,
.symbol-item strong,
.journal-item strong {
display: block;
margin-bottom: 4px;
}
.line-item p,
.symbol-item p,
.journal-item p {
margin: 0;
color: var(--muted);
line-height: 1.6;
font-size: 0.94rem;
}
.line-item.moving {
padding-left: 12px;
box-shadow: inset 3px 0 0 var(--cinnabar);
}
.line-item.selected {
padding-left: 12px;
background: rgba(31, 111, 99, 0.06);
box-shadow: inset 3px 0 0 var(--jade);
}
.line-item.moving.selected {
box-shadow:
inset 3px 0 0 var(--cinnabar),
inset 7px 0 0 var(--jade);
}
.source-text {
color: var(--ink) !important;
font-family: "Songti SC", "STSong", "Noto Serif CJK SC", serif;
}
.journal-panel {
max-height: 420px;
overflow: auto;
}
.journal-item {
width: 100%;
text-align: left;
cursor: pointer;
}
.journal-item:hover {
color: var(--jade);
}
@media (max-width: 980px) {
.oracle,
.result-layout,
.detail-grid {
grid-template-columns: 1fr;
}
.catalog-grid,
.hexagram-library {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.masthead {
min-height: 300px;
}
.hexagram-visuals {
grid-template-columns: 1fr;
}
.change-arrow {
justify-self: center;
transform: rotate(90deg);
}
}
@media (max-width: 620px) {
.shell {
width: min(100% - 18px, 1180px);
padding-top: 10px;
}
.masthead,
.casting-panel,
.hexagram-stage,
.reading-panel,
.detail-panel {
padding: 18px;
}
.controls {
align-items: stretch;
}
.mode-switch,
.cast-button {
width: 100%;
}
.section-heading {
align-items: stretch;
flex-direction: column;
}
.atlas-search,
.catalog-grid,
.hexagram-library {
width: 100%;
grid-template-columns: 1fr;
}
.hex-block {
min-height: 360px;
padding: 18px;
}
.hexagram {
width: min(100%, 210px);
}
}
FILE:references/zhouyi-benjing-source.txt
周易本经
整理日期:2026-04-19
来源:Project Gutenberg eBook #25501《易經》。
整理范围:六十四卦卦辞、爻辞;不含《彖传》《象传》《文言》《系辞》《说卦》《序卦》《杂卦》。
第 一 卦
乾
乾:元,亨,利,貞。
初九:潛龍,勿用。
九二:見龍在田,利見大人。
九三:君子終日乾乾,夕惕,若厲,無咎。
九四:或躍在淵,無咎。
九五:飛龍在天,利見大人。
上九:亢龍有悔。
用九:見群龍無首,吉。
第 二 卦
坤
坤:元,亨,利牝馬之貞。
君子有攸往,先迷後得主,利西南得朋,東北喪朋。安貞,吉。
初六:履霜,堅冰至。
六二:直,方,大,不習無不利。
六三:含章可貞。或從王事,無成有終。
六四:括囊;無咎,無譽。
六五:黃裳,元吉。
上六:戰龍於野,其血玄黃。
用六:利永貞。
第 三 卦
屯
屯:元,亨,利,貞,勿用,有攸往,利建侯。
初九:磐桓;利居貞,利建侯。
六二:屯如邅如,乘馬班如。匪寇婚媾,女子貞不字,十年乃字。
六三:既鹿無虞,惟入于林中,君子幾不如舍,往吝。
六四:乘馬班如,求婚媾,往吉,無不利。
九五:屯其膏,小貞吉,大貞凶。
上六:乘馬班如,泣血漣如。
第 四 卦
蒙
蒙:亨。匪我求童蒙,童蒙求我。初噬告,再三瀆,瀆則不告。利貞。
初六:發蒙,利用刑人,用說桎梏,以往吝。
九二:包蒙,吉;納婦,吉;子克家。
六三:勿用娶女;見金夫,不有躬,無攸利。
六四:困蒙,吝。
六五:童蒙,吉。
上九:擊蒙;不利為寇,利御寇。
第 五 卦
需
需:有孚,光亨,貞吉。利涉大川。
初九:需于郊。利用恆,無咎。
九二:需于沙。小有言,終吉。
九三:需于泥,致寇至。
六四:需于血,出自穴。
九五:需于酒食,貞吉。
上六:入于穴,有不速之客三人來,敬之終吉。
第 六 卦
訟
訟:有孚,窒。惕中吉。終凶。利見大人,不利涉大川。
初六:不永所事,小有言,終吉。
九二:不克訟,歸而逋,其邑人三百戶,無眚。
六三:食舊德,貞厲,終吉,或從王事,無成。
九四:不克訟,復即命,渝安貞,吉。
九五:訟元吉。
上九:或錫之鞶帶,終朝三褫之。
第 七 卦
師
師:貞,丈人,吉無咎。
初六:師出以律,否臧凶。
九二:在師中,吉無咎,王三錫命。
六三:師或輿尸,凶。
六四:師左次,無咎。
六五:田有禽,利執言,無咎。長子帥師,弟子輿尸,貞凶。
上六:大君有命,開國承家,小人勿用。
第 八 卦
比
比:吉。原筮元永貞,無咎。不寧方來,後夫凶。
初六:有孚比之,無咎。有孚盈缶,終來有他,吉。
六二:比之自內,貞吉。
六三:比之匪人。
六四:外比之,貞吉。
九五:顯比,王用三驅,失前禽。邑人不誡,吉。
上六:比之無首,凶。
第 九 卦
小畜
小畜:亨。密雲不雨,自我西郊。
初九:復自道,何其咎,吉。
九二:牽復,吉。
九三:輿說輻,夫妻反目。
六四:有孚,血去。惕出,無咎。
九五:有孚攣如,富以其鄰。
上九:既雨既處,尚德載婦,貞厲。月幾望,君子征凶。
第 十 卦
履
履:履虎尾,不咥人,亨。
初九:素履,往無咎。
九二:履道坦坦,幽人貞吉。
六三:眇能視,跛能履,履虎尾,咥人,凶。武人為于大君。
九四:履虎尾,愬愬,終吉。
九五:夬履,貞厲。
上九:視履考祥,其旋元吉。
第 十一 卦
泰
泰:小往大來,吉亨。
初九:拔茅茹,以其彙,征吉。
九二:包荒,用馮河,不遐遺,朋亡,得尚于中行。
九三:無平不陂,無往不復,艱貞無咎。勿恤其孚,于食有福。
六四:翩翩不富,以其鄰,不戒以孚。
六五:帝乙歸妹,以祉元吉。
上六:城復于隍,勿用師。自邑告命,貞吝。
第 十二 卦
否
否:否之匪人,不利君子貞,大往小來。
初六:拔茅茹,以其彙,貞吉亨。
六二:包承。小人吉,大人否亨。
六三:包羞。
九四:有命無咎,疇離祉。
九五:休否,大人吉。其亡其亡,繫于苞桑。
上九:傾否,先否後喜。
第 十三 卦
同人
同人:同人于野,亨。利涉大川,利君子貞。
初九:同人于門,無咎。
六二:同人于宗,吝。
九三:伏戎于莽,升其高陵,三歲不興。
九四:乘其墉,弗克攻,吉。
九五:同人,先號咷而後笑。大師克相遇。
上九:同人于郊,無悔。
第 十四 卦
大有
大有:元亨。
初九:無交害,匪咎,艱則無咎。
九二:大車以載,有攸往,無咎。
九三:公用亨于天子,小人弗克。
九四:匪其彭,無咎。
六五:厥孚交如,威如;吉。
上九:自天佑之,吉無不利。
第 十五 卦
謙
謙:亨,君子有終。
初六:謙謙君子,用涉大川,吉。
六二:鳴謙,貞吉。
九三:勞謙君子,有終吉。
六四:無不利,撝謙。
六五:不富,以其鄰,利用侵伐,無不利。
上六:鳴謙,利用行師,征邑國。
第 十六 卦
豫
豫:利建侯行師。
初六:鳴豫,凶。
六二:介于石,不終日,貞吉。
六三:盱豫,悔。遲有悔。
九四:由豫,大有得。勿疑。朋盍簪。
六五:貞疾,恆不死。
上六:冥豫,成有渝,無咎。
第 十七 卦
隨
隨:元亨利貞,無咎。
初九:官有渝,貞吉。出門交有功。
六二:係小子,失丈夫。
六三:係丈夫,失小子。隨有求得,利居貞。
九四:隨有獲,貞凶。有孚在道,以明,何咎。
九五:孚于嘉,吉。
上六:拘系之,乃從維之。王用亨于西山。
第 十八 卦
蠱
蠱:元亨,利涉大川。先甲三日,後甲三日。
初六:幹父之蠱,有子,考無咎,厲終吉。
九二:幹母之蠱,不可貞。
九三:幹父小有晦,無大咎。
六四:裕父之蠱,往見吝。
六五:幹父之蠱,用譽。
上九:不事王侯,高尚其事。
第 十九 卦
臨
臨:元,亨,利,貞。至于八月有凶。
初九:咸臨,貞吉。
九二:咸臨,吉無不利。
六三:甘臨,無攸利。既憂之,無咎。
六四:至臨,無咎。
六五:知臨,大君之宜,吉。
上六:敦臨,吉無咎。
第 二十 卦
觀
觀:盥而不薦,有孚顒若。
初六:童觀,小人無咎,君子吝。
六二:窺觀,利女貞。
六三:觀我生,進退。
六四:觀國之光,利用賓于王。
九五:觀我生,君子無咎。
上九:觀其生,君子無咎。
第二十一卦
噬嗑
噬嗑:亨。利用獄。
初九:履校滅趾,無咎。
六二:噬膚滅鼻,無咎。
六三:噬臘肉,遇毒;小吝,無咎。
九四:噬乾胏,得金矢,利艱貞,吉。
六五:噬乾肉,得黃金,貞厲,無咎。
上九:何校滅耳,凶。
第二十二卦
賁
賁:亨。小利有所往。
初九:賁其趾,舍車而徒。
六二:賁其須。
九三:賁如濡如,永貞吉。
六四:賁如皤如,白馬翰如,匪寇婚媾。
六五:賁于丘園,束帛戔戔,吝,終吉。
上九:白賁,無咎。
第二十三卦
剝
剝:不利有攸往。
初六:剝床以足,蔑貞凶。
六二:剝床以辨,蔑貞凶。
六三:剝之,無咎。
六四:剝床以膚,凶。
六五:貫魚,以宮人寵,無不利。
上九:碩果不食,君子得輿,小人剝廬。
第二十四卦
復
復:亨。出入無疾,朋來無咎。反復其道,七日來復,利有攸往。
初九:不復遠,無祗悔,元吉。
六二:休復,吉。
六三:頻復厲,無咎。
六四:中行獨復。
六五:敦復,無悔。
上六:迷復,凶,有災眚。用行師,終有大敗,以其國君,凶;至于十年,不克征。
第二十五卦
無妄
無妄:元,亨,利,貞。其匪正有眚,不利有攸往。
初九:無妄,往吉。
六二:不耕獲,不菑畬,則利有攸往。
六三:無妄之災,或繫之牛,行人之得,邑人之災。
九四:可貞,無咎。
九五:無妄之疾,勿藥有喜。
上九:無妄,行有眚,無攸利。
第二十六卦
大畜
大畜:利貞,不家食吉,利涉大川。
初九:有厲利已。
九二:輿說輻。
九三:良馬逐,利艱貞。日閑輿衛,利有攸往。
六四:童牛之牿,元吉。
六五:豶豕之牙,吉。
上九:何天之衢,亨。
第二十七卦
頤
頤:貞吉。觀頤,自求口實。
初九:舍爾靈龜,觀我朵頤,凶。
六二:顛頤,拂經于丘頤,征凶。
六三:拂頤,貞凶,十年勿用,無攸利。
六四:顛頤吉,虎視眈眈,其欲逐逐,無咎。
六五:拂經,居貞吉,不可涉大川。
上九:由頤,厲吉,利涉大川。
第二十八卦
大過
大過:棟橈,利有攸往,亨。
初六:藉用白茅,無咎。
九二:枯楊生稊,老夫得其女妻,無不利。
九三:棟橈,凶。
九四:棟隆,吉;有它吝。
九五:枯楊生華,老婦得士夫,無咎無譽。
上六:過涉滅頂,凶,無咎。
第二十九卦
坎
坎:習坎,有孚,維心亨,行有尚。
初六:習坎,入于坎窞,凶。
九二:坎有險,求小得。
六三:來之坎坎,險且枕,入于坎窞,勿用。
六四:樽酒簋貳,用缶,納約自牖,終無咎。
九五:坎不盈,祗既平,無咎。
上六:係用徽纆,寘于叢棘,三歲不得,凶。
第 三十 卦
離
離:利貞,亨。畜牝牛,吉。
初九:履錯然,敬之無咎。
六二:黃離,元吉。
九三:日昃之離,不鼓缶而歌,則大耋之嗟,凶。
九四:突如其來如,焚如,死如,棄如。
六五:出涕沱若,戚嗟若,吉。
上九:王用出征,有嘉折首,獲匪其醜,無咎。
第三十一卦
咸
咸:亨,利貞,取女吉。
初六:咸其拇。
六二:咸其腓,凶,居吉。
九三:咸其股,執其隨,往吝。
九四:貞吉悔亡,憧憧往來,朋從爾思。
九五:咸其脢,無悔。
上六:咸其輔,頰,舌。
第三十二卦
恆:亨,無咎,利貞,利有攸往。
初六:浚恆,貞凶,無攸利。
九二:悔亡。
九三:不恆其德,或承之羞,貞吝。
九四:田無禽。
六五:恆其德,貞,婦人吉,夫子凶。
上六:振恆,凶。
第三十三卦
遯
遯:亨,小利貞。
初六:遯尾,厲,勿用有攸往。
六二:執之用黃牛之革,莫之勝說。
九三:係遯,有疾厲,畜臣妾吉。
九四:好遯君子吉,小人否。
九五:嘉遯,貞吉。
上九:肥遯,無不利。
第三十四卦
大壯
大壯:利貞。
初九:壯于趾,征凶,有孚。
九二:貞吉。
九三:小人用壯,君子用罔,貞厲。羝羊觸藩,羸其角。
九四:貞吉悔亡,藩決不羸,壯于大輿之輹。
六五:喪羊于易,無悔。
上六:羝羊觸藩,不能退,不能遂,無攸利,艱則吉。
第三十五卦
晉
晉:康侯用錫馬蕃庶,晝日三接。
初六:晉如,摧如,貞吉。罔孚,裕無咎。
六二:晉如,愁如,貞吉。受茲介福,于其王母。
六三:眾允,悔亡。
九四:晉如鼫鼠,貞厲。
六五:悔亡,失得勿恤,往吉無不利。
上九:晉其角,維用伐邑,厲吉無咎,貞吝。
第三十六卦
明夷
明夷:利艱貞。
初九:明夷于飛,垂其翼。君子于行,三日不食,有攸往,主人有言。
六二:明夷,夷于左股,用拯馬壯,吉。
九三:明夷于南狩,得其大首,不可疾貞。
六四:入于左腹,獲明夷之心,于出門庭。
六五:箕子之明夷,利貞。
上六:不明晦,初登于天,後入于地。
第三十七卦
家人
家人:利女貞。
初九:閑有家,悔亡。
六二:無攸遂,在中饋,貞吉。
九三:家人嗃嗃,悔厲吉;婦子嘻嘻,終吝。
六四:富家,大吉。
九五:王假有家,勿恤吉。
上九:有孚威如,終吉。
第三十八卦
睽
睽:小事吉。
初九:悔亡,喪馬勿逐,自復;見惡人無咎。
九二:遇主于巷,無咎。
六三:見輿曳,其牛掣,其人天且劓,無初有終。
九四:睽孤,遇元夫,交孚,厲無咎。
六五:悔亡,厥宗噬膚,往何咎。
上九:睽孤,見豕負塗,載鬼一車,先張之弧,後說之弧,匪寇婚媾,往遇雨則吉。
第三十九卦
蹇
蹇:利西南,不利東北;利見大人,貞吉。
初六:往蹇,來譽。
六二:王臣蹇蹇,匪躬之故。
九三:往蹇來反。
六四:往蹇來連。
九五:大蹇朋來。
上六:往蹇來碩,吉;利見大人。
第 四十 卦
解
解:利西南,無所往,其來復吉。有攸往,夙吉。
初六:無咎。
九二:田獲三狐,得黃矢,貞吉。
六三:負且乘,致寇至,貞吝。
九四:解而拇,朋至斯孚。
六五:君子維有解,吉;有孚于小人。
上六:公用射隼,于高墉之上,獲之,無不利。
第四十一卦
損
損:有孚,元吉,無咎,可貞,利有攸往?曷之用,二簋可用享。
初九:已事遄往,無咎,酌損之。
九二:利貞,征凶,弗損益之。
六三:三人行,則損一人;一人行,則得其友。
六四:損其疾,使遄有喜,無咎。
六五:或益之,十朋之龜弗克違,元吉。
上九:弗損益之,無咎,貞吉,利有攸往,得臣無家。
第四十二卦
益
益:利有攸往,利涉大川。
初九:利用為大作,元吉,無咎。
六二:或益之,十朋之龜弗克違,永貞吉。王用享于帝,吉。
六三:益之用凶事,無咎。有孚中行,告公用圭。
六四:中行,告公從。利用為依遷國。
九五:有孚惠心,勿問元吉。有孚惠我德。
上九:莫益之,或擊之,立心勿恆,凶。
第四十三卦
夬
夬:揚于王庭,孚號,有厲,告自邑,不利即戎,利有攸往。
初九:壯于前趾,往不勝為吝。
九二:惕號,莫夜有戎,勿恤。
九三:壯于頄,有凶。君子夬夬,獨行遇雨,若濡有慍,無咎。
九四:臀無膚,其行次且。牽羊悔亡,聞言不信。
九五:莧陸夬夬,中行無咎。
上六:無號,終有凶。
第四十四卦
姤
姤:女壯,勿用取女。
初六:繫于金柅,貞吉,有攸往,見凶,羸豕孚蹢躅。
九二:包有魚,無咎,不利賓。
九三:臀無膚,其行次且,厲,無大咎。
九四:包無魚,起凶。
九五:以杞包瓜,含章,有隕自天。
上九:姤其角,吝,無咎。
第四十五卦
萃
萃:亨。王假有廟,利見大人,亨,利貞。用大牲吉,利有攸往。
初六:有孚不終,乃亂乃萃,若號一握為笑,勿恤,往無咎。
六二:引吉,無咎,孚乃利用禴。
六三:萃如,嗟如,無攸利,往無咎,小吝。
九四:大吉,無咎。
九五:萃有位,無咎。匪孚,元永貞,悔亡。
上六:齎咨涕洟,無咎。
第四十六卦
升
升:元亨,用見大人,勿恤,南征吉。
初六:允升,大吉。
九二:孚乃利用禴,無咎。
九三:升虛邑。
六四:王用亨于岐山,吉無咎。
六五:貞吉,升階。
上六:冥升,利于不息之貞。
第四十七卦
困
困:亨,貞,大人吉,無咎,有言不信。
初六:臀困于株木,入于幽谷,三歲不覿。
九二:困于酒食,朱紱方來,利用亨祀,征凶,無咎。
六三:困于石,據于蒺藜,入于其宮,不見其妻,凶。
九四:來徐徐,困于金車,吝,有終。
九五:劓刖,困于赤紱,乃徐有說,利用祭祀。
上六:困于葛藟,于臲卼,曰動悔。有悔,征吉。
第四十八卦
井
井:改邑不改井,無喪無得,往來井井。汔至,亦未繘井,羸其瓶,凶。
初六:井泥不食,舊井無禽。
九二:井谷射鮒,甕敝漏。
九三:井渫不食,為我民惻,可用汲,王明,並受其福。
六四:井甃,無咎。
九五:井冽,寒泉食。
上六:井收勿幕,有孚無吉。
第四十九卦
革
革:己日乃孚,元亨利貞,悔亡。
初九:鞏用黃牛之革。
六二:己日乃革之,征吉,無咎。
九三:征凶,貞厲,革言三就,有孚。
九四:悔亡,有孚改命,吉。
九五:大人虎變,未占有孚。
上六:君子豹變,小人革面,征凶,居貞吉。
第 五十 卦
鼎
鼎:元吉,亨。
初六:鼎顛趾,利出否,得妾以其子,無咎。
九二:鼎有實,我仇有疾,不我能即,吉。
九三:鼎耳革,其行塞,雉膏不食,方雨虧悔,終吉。
九四:鼎折足,覆公餗,其形渥,凶。
六五:鼎黃耳金鉉,利貞。
上九:鼎玉鉉,大吉,無不利。
第五十一卦
震
震:亨。震來虩虩,笑言啞啞。震驚百里,不喪匕鬯。
初九:震來虩虩,後笑言啞啞,吉。
六二:震來厲,億喪貝,躋于九陵,勿逐,七日得。
六三:震蘇蘇,震行無眚。
九四:震遂泥。
六五:震往來厲,億無喪,有事。
上六:震索索,視矍矍,征凶。震不于其躬,于其鄰,無咎。婚媾有言。
第五十二卦
艮
艮:艮其背,不獲其身,行其庭,不見其人,無咎。
初六:艮其趾,無咎,利永貞。
六二:艮其腓,不拯其隨,其心不快。
九三:艮其限,列其夤,厲薰心。
六四:艮其身,無咎。
六五:艮其輔,言有序,悔亡。
上九:敦艮,吉。
第五十三卦
漸
漸:女歸吉,利貞。
初六:鴻漸于干,小子厲,有言,無咎。
六二:鴻漸于磐,飲食衎衎,吉。
九三:鴻漸于陸,夫征不復,婦孕不育,凶;利御寇。
六四:鴻漸于木,或得其桷,無咎。
九五:鴻漸于陵,婦三歲不孕,終莫之勝,吉。
上九:鴻漸于逵,其羽可用為儀,吉。
第五十四卦
歸妹
歸妹:征凶,無攸利。
初九:歸妹以娣,跛能履,征吉。
九二:眇能視,利幽人之貞。
六三:歸妹以須,反歸以娣。
九四:歸妹愆期,遲歸有時。
六五:帝乙歸妹,其君之袂,不如其娣之袂良,月幾望,吉。
上六:女承筐無實,士刲羊無血,無攸利。
第五十五卦
豐
豐:亨,王假之,勿憂,宜日中。
初九:遇其配主,雖旬無咎,往有尚。
六二:豐其蔀,日中見斗,往得疑疾,有孚發若,吉。
九三:豐其沛,日中見沫,折其右肱,無咎。
九四:豐其蔀,日中見斗,遇其夷主,吉。
六五:來章,有慶譽,吉。
上六:豐其屋,蔀其家,窺其戶,闃其無人,三歲不覿,凶。
第五十六卦
旅
旅:小亨,旅貞吉。
初六:旅瑣瑣,斯其所取災。
六二:旅即次,懷其資,得童僕貞。
九三:旅焚其次,喪其童僕,貞厲。
九四:旅于處,得其資斧,我心不快。
六五:射雉一矢亡,終以譽命。
上九:鳥焚其巢,旅人先笑後號咷。喪牛于易,凶。
第五十七卦
巽
巽:小亨,利攸往,利見大人。
初六:進退,利武人之貞。
九二:巽在床下,用史巫紛若,吉無咎。
九三:頻巽,吝。
六四:悔亡,田獲三品。
九五:貞吉悔亡,無不利。無初有終,先庚三日,後庚三日,吉。
上九:巽在床下,喪其資斧,貞凶。
第五十八卦
兌
兌:亨,利貞。
初九:和兌,吉。
九二:孚兌,吉,悔亡。
六三:來兌,凶。
九四:商兌,未寧,介疾有喜。
九五:孚于剝,有厲。
上六:引兌。
第五十九卦
渙
渙:亨。王假有廟,利涉大川,利貞。
初六:用拯馬壯,吉。
九二:渙奔其機,悔亡。
六三:渙其躬,無悔。
六四:渙其群,元吉。渙有丘,匪夷所思。
九五:渙汗其大號,渙王居,無咎。
上九:渙其血,去逖出,無咎。
第 六十 卦
節
節:亨。苦節不可貞。
初九:不出戶庭,無咎。
九二:不出門庭,凶。
六三:不節若,則嗟若,無咎。
六四:安節,亨。
九五:甘節,吉;往有尚。
上六:苦節,貞凶,悔亡。
第六十一卦
中孚
中孚:豚魚,吉,利涉大川,利貞。
初九:虞吉,有他不燕。
九二:鳴鶴在陰,其子和之,我有好爵,吾與爾靡之。
六三:得敵,或鼓或罷,或泣或歌。
六四:月幾望,馬匹亡,無咎。
九五:有孚攣如,無咎。
上九:翰音登于天,貞凶。
第六十二卦
小過
小過:亨,利貞,可小事,不可大事。飛鳥遺之音,不宜上宜下,大吉。
初六:飛鳥以凶。
六二:過其祖,遇其妣;不及其君,遇其臣;無咎。
九三:弗過防之,從或戕之,凶。
九四:無咎,弗過遇之。往厲必戒,勿用永貞。
六五:密雲不雨,自我西郊,公弋取彼在穴。
上六:弗遇過之,飛鳥離之,凶,是謂災眚。
第六十三卦
既濟
既濟:亨,小利貞,初吉終亂。
初九:曳其輪,濡其尾,無咎。
六二:婦喪其茀,勿逐,七日得。
九三:高宗伐鬼方,三年克之,小人勿用。
六四:繻有衣袽,終日戒。
九五:東鄰殺牛,不如西鄰之禴祭,實受其福。
上六:濡其首,厲。
第六十四卦
未濟
未濟:亨,小狐汔濟,濡其尾,無攸利。
初六:濡其尾,吝。
九二:曳其輪,貞吉。
六三:未濟,征凶,利涉大川。
九四:貞吉,悔亡,震用伐鬼方,三年有賞于大國。
六五:貞吉,無悔,君子之光,有孚,吉。
上九:有孚于飲酒,無咎,濡其首,有孚失是。
FILE:references/user-guide-zh.md
# 周易本经占筮:使用说明
这不是一个只会“给吉凶”的算卦玩具,而是一套基于《周易》本经原著、适合做决策整理和处境判断的系统。
你可以把它当成三种工具来用:
1. `起卦`:问一件事现在该怎么推进。
2. `查卦`:查六十四卦原文、卦辞、爻辞。
3. `导航`:看周易和六爻、梅花、八字、奇门等体系有什么区别。
## 一、最适合拿来问什么
这套系统最适合的问题是:
- 我该不该推进这次合作?
- 这份工作现在该继续,还是先缓一缓?
- 这段关系下一步适合主动沟通,还是先观察?
- 这个项目目前最大的风险点是什么?
- 我现在应该进,还是守?
不适合的问题是:
- 我什么时候一定发财?
- 对方百分百会不会回来?
- 哪只股票一定涨?
- 医疗、法律、投资上的确定指令
一句话:它更适合看`处境`、`节奏`、`取舍`和`提醒`,不适合替你承担现实决策责任。
## 二、在 ClawHub / 对话里怎么用
你可以直接对它说下面这些话。
### 1. 直接起卦
示例:
- 帮我起一卦,问这次合作要不要推进
- 用周易看一下我现在是否适合换工作
- 用三钱起卦,看看这段关系接下来该怎么处理
- 用蓍草法帮我问一下这个项目的时机
推荐说法:
```text
帮我起一卦,问题是:我这个月是否应该主动推进新的合作?
```
### 2. 直接查某一卦
示例:
- 查乾卦
- 第三卦是什么
- 给我看屯卦的卦辞和六二爻辞
- 查一下“利涉大川”出自哪些卦
### 3. 让它解释一个结果
示例:
- 这个卦为什么取这一条爻辞?
- 两爻变应该怎么看?
- 请先给我原文,再给白话解释
- 不要鸡汤,直接说这卦提醒我什么
### 4. 让它做体系导航
示例:
- 周易和六爻有什么区别?
- 小六壬是不是基于周易原文?
- 这个问题更适合用周易、八字还是奇门?
## 三、问得越好,结果越准
最好的提问方式是:`一事一问`。
好问题:
- 我这个月要不要推进新的合作?
- 我现在是否适合离开这份工作?
- 这段关系接下来该不该主动沟通?
不好的问法:
- 我的整个人生怎么样?
- 我什么时候一定暴富?
- 他到底爱不爱我、会不会回来、会不会娶我?
你可以照着这个模板问:
```text
我想问的是:
现在这件事的核心问题是:
我最想判断的是:该推进 / 该等待 / 该止损 / 该沟通?
```
## 四、结果出来后怎么看
结果里最重要的是四部分:
- `本卦`:你现在的处境是什么
- `变卦`:事情接下来可能往哪里转
- `动爻`:变化最关键的位置
- `本经取辞`:真正该看的《周易》原文
最简单的读法:
1. 先看系统说这次用了哪条规则取辞。
2. 再看引用的卦辞或爻辞原文。
3. 最后再看它对现实问题的白话解释。
不要一上来只盯着“吉”还是“凶”。
真正有用的是:它提醒你现在该进、该守、该改,还是该停。
## 五、七种取辞规则
这套系统严格按传统的变爻规则来,不会乱选一句听起来顺耳的话:
1. 六爻不变:看本卦卦辞
2. 一爻变:看该动爻爻辞
3. 二爻变:看两条动爻,以上爻为主
4. 三爻变:看本卦卦辞和变卦卦辞
5. 四爻变:看两条静爻,以下爻为主
6. 五爻变:看变卦中唯一静爻
7. 六爻皆变:乾看用九,坤看用六,其余看变卦卦辞
这也是它和很多“会起卦但不会严格取辞”的同类 agent 最大的不同之一。
## 六、如果你是本地打开网页用
打开:
`index.html`
你会看到三个主要区域:
### 1. 周易本经占筮
- 输入问题
- 选择 `三钱` 或 `蓍草`
- 点击 `起卦`
### 2. 本经卦库
可以搜索:
- 卦名:乾、坤、屯、需……
- 卦辞关键词:利涉大川、元亨利贞……
- 爻辞关键词:十年乃字、无咎……
### 3. 术数百科
这里会告诉你:
- 哪些体系已经校验过
- 哪些体系暂时只做百科导航
- 每个体系适合解决什么问题
## 七、推荐的使用姿势
如果你是第一次用,最推荐这三种方式:
### 方式一:直接问现实问题
```text
帮我起一卦,问这次合作还要不要继续推进。
```
### 方式二:先查卦再理解
```text
查一下谦卦,并告诉我它为什么常被理解成“有利于长期发展”。
```
### 方式三:先问体系差异
```text
我现在的问题到底适合用周易本经、六爻还是八字?
```
## 八、边界说明
这套系统有一个很重要的原则:
`宁可说清边界,也不冒充高精度。`
所以它会坚持这些做法:
- 周易部分只以本经原文为底
- 不把八字、奇门、紫微、小六壬硬混进周易解释
- 不把白话改写伪装成原文
- 不把未校验的命理模块装成“全都能算”
这也是它和很多市面上“什么都想算、但每样都不够扎实”的 agent 不一样的地方。
## 九、一句话总结
最好的使用方法不是把它当神谕机,而是把它当一面镜子:
先用《周易》帮你把问题照清楚,再回到现实里做真正的选择。
FILE:references/README.md
# 周易本经占筮
一个以 `sources/zhouyi/zhouyi_benjing.txt` 为底座的本地《周易》占筮系统,同时提供百科式术数导航。
如果你只是想知道怎么使用,请先看:`使用说明-小白版.md`。
## 核心原则
- 以《周易》本经卦辞、爻辞、用九、用六为第一数据源。
- 起卦只负责生成六爻,不把随机结果包装成确定命令。
- 解读必须先说明取辞规则,再引用本经原文,再给出现代语境建议。
- 其他术数体系只做百科导航和资料要求说明;未通过校验前不冒充高精度排盘。
- 高风险问题只给决策提示,不替代医疗、法律、投资等专业意见。
## 已吸收并修正的点
- 保留 `gua` 包里较好的 64 卦映射、三钱概率和七种取辞规则。
- 修正之前多个包里出现的卦序反置、原文截断、白话混入原文等问题。
- 放弃不可靠的八字、六爻纳甲、奇门、紫微混搭,不让非周易体系污染本经解释。
- 增加数据构建校验,确保 64 卦完整、每卦 6 爻、乾坤特殊用辞存在。
- 新增全体系百科导航,按 S/B/C 级标明可信度、资料要求和边界。
- 新增本经卦库检索,可按卦名、卦辞、爻辞关键词查找。
## 使用
直接用浏览器打开 `index.html`。
如果想接到 Telegram,见:`TELEGRAM部署.md`。
可选起卦方式:
- `三钱`:三枚铜钱法,6/7/8/9 概率为 1/8、3/8、3/8、1/8。
- `蓍草`:按蓍草法常用概率模拟,6/7/8/9 概率为 1/16、5/16、7/16、3/16。
## 取辞规则
- 六爻不变:用本卦卦辞。
- 一爻变:用该动爻爻辞。
- 二爻变:用两个动爻爻辞,以上爻为主。
- 三爻变:用本卦卦辞与变卦卦辞。
- 四爻变:用两个静爻爻辞,以下爻为主。
- 五爻变:用变卦中唯一静爻所对应的爻辞。
- 六爻皆变:乾用用九,坤用用六,其他卦用变卦卦辞。
## 数据构建
```bash
python3 tools/build_zhouyi_data.py
```
这会从 `sources/zhouyi/zhouyi_benjing.txt` 生成 `data/zhouyi-benjing.js`。
## 验证
```bash
node tests/verify_zhouyi_system.js
node tests/verify_system_catalog.js
```
验证内容包括:64 卦数量、每卦 6 爻、乾坤用辞、关键原文、乾坤泰否既济未济等卦象映射。
百科验证内容包括:体系数量、字段完整性、已启用模块范围、待校验体系边界。
FILE:tests/verify_zhouyi_system.js
#!/usr/bin/env node
const assert = require("node:assert/strict");
const zhouyi = require("../data/zhouyi-benjing.js");
const trigrams = {
"111": "乾",
"110": "兑",
"101": "离",
"100": "震",
"011": "巽",
"010": "坎",
"001": "艮",
"000": "坤"
};
const lookup = {
"乾|乾": 1, "乾|兑": 43, "乾|离": 14, "乾|震": 34, "乾|巽": 9, "乾|坎": 5, "乾|艮": 26, "乾|坤": 11,
"兑|乾": 10, "兑|兑": 58, "兑|离": 38, "兑|震": 54, "兑|巽": 61, "兑|坎": 60, "兑|艮": 41, "兑|坤": 19,
"离|乾": 13, "离|兑": 49, "离|离": 30, "离|震": 55, "离|巽": 37, "离|坎": 63, "离|艮": 22, "离|坤": 36,
"震|乾": 25, "震|兑": 17, "震|离": 21, "震|震": 51, "震|巽": 42, "震|坎": 3, "震|艮": 27, "震|坤": 24,
"巽|乾": 44, "巽|兑": 28, "巽|离": 50, "巽|震": 32, "巽|巽": 57, "巽|坎": 48, "巽|艮": 18, "巽|坤": 46,
"坎|乾": 6, "坎|兑": 47, "坎|离": 64, "坎|震": 40, "坎|巽": 59, "坎|坎": 29, "坎|艮": 4, "坎|坤": 7,
"艮|乾": 33, "艮|兑": 31, "艮|离": 56, "艮|震": 62, "艮|巽": 53, "艮|坎": 39, "艮|艮": 52, "艮|坤": 15,
"坤|乾": 12, "坤|兑": 45, "坤|离": 35, "坤|震": 16, "坤|巽": 20, "坤|坎": 8, "坤|艮": 23, "坤|坤": 2
};
function resolve(bits) {
const lower = trigrams[bits.slice(0, 3)];
const upper = trigrams[bits.slice(3, 6)];
return zhouyi[lookup[`lower|upper`] - 1];
}
assert.equal(zhouyi.length, 64);
for (const hex of zhouyi) {
assert.equal(hex.lines.length, 6, `hex.number hex.name should have 6 lines`);
assert.ok(hex.judgment, `hex.number hex.name should have judgment`);
}
assert.equal(zhouyi[0].name, "乾");
assert.equal(zhouyi[0].extras[0].label, "用九");
assert.equal(zhouyi[1].name, "坤");
assert.equal(zhouyi[1].extras[0].label, "用六");
assert.equal(zhouyi[2].lines[1].text, "屯如邅如,乘馬班如。匪寇婚媾,女子貞不字,十年乃字。");
assert.equal(resolve("111111").name, "乾");
assert.equal(resolve("000000").name, "坤");
assert.equal(resolve("111000").name, "泰");
assert.equal(resolve("000111").name, "否");
assert.equal(resolve("101010").name, "既濟");
assert.equal(resolve("010101").name, "未濟");
assert.equal(resolve("011101").name, "鼎");
assert.equal(resolve("001101").name, "旅");
const sourceRules = [
"六爻不变:本卦卦辞",
"一爻变:该动爻爻辞",
"二爻变:两条动爻爻辞,以上爻为主",
"三爻变:本卦卦辞与变卦卦辞",
"四爻变:两条静爻爻辞,以下爻为主",
"五爻变:变卦中唯一静爻所对应的爻辞",
"六爻皆变:乾用用九,坤用用六,其他用变卦卦辞"
];
assert.equal(sourceRules.length, 7);
console.log("zhouyi system verification passed");
FILE:tests/verify_cli.js
const { execFileSync } = require("child_process");
const path = require("path");
const root = path.join(__dirname, "..");
const cli = path.join(root, "scripts", "zhouyi_cli.js");
function run(args) {
return execFileSync("node", [cli, ...args], {
cwd: root,
encoding: "utf-8"
});
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
const cast = JSON.parse(run(["cast", "--question", "测试", "--method", "coin", "--seed", "demo", "--json"]));
assert(cast.primary && cast.primary.number >= 1 && cast.primary.number <= 64, "cast should resolve primary hexagram");
assert(cast.changed && cast.changed.number >= 1 && cast.changed.number <= 64, "cast should resolve changed hexagram");
assert(Array.isArray(cast.lines) && cast.lines.length === 6, "cast should return 6 lines");
assert(cast.decision && cast.decision.entries.length >= 1, "cast should return decision entries");
const lookup = JSON.parse(run(["lookup", "--name", "乾", "--json"]));
assert(lookup.number === 1, "lookup by name should return 乾卦");
assert(lookup.extras[0].label === "用九", "乾卦 should retain 用九");
const search = JSON.parse(run(["search", "--query", "十年乃字", "--json"]));
assert(search.some((item) => item.number === 3), "search should find 屯卦");
const catalog = JSON.parse(run(["catalog", "--grade", "S", "--json"]));
assert(catalog.length >= 3, "catalog S grade should include enabled core systems");
console.log("cli verification passed");
FILE:tests/verify_system_catalog.js
#!/usr/bin/env node
const assert = require("node:assert/strict");
const systems = require("../data/system-catalog.js");
assert.ok(systems.length >= 10, "catalog should feel encyclopedic");
for (const item of systems) {
assert.ok(item.id, "system id required");
assert.ok(item.name, `item.id name required`);
assert.ok(["S", "B", "C"].includes(item.grade), `item.id invalid grade`);
assert.ok(item.status, `item.id status required`);
assert.ok(item.basis, `item.id basis required`);
assert.ok(item.capability, `item.id capability required`);
assert.ok(item.guardrail, `item.id guardrail required`);
assert.ok(Array.isArray(item.bestFor) && item.bestFor.length > 0, `item.id bestFor required`);
assert.ok(Array.isArray(item.inputs) && item.inputs.length > 0, `item.id inputs required`);
}
const active = systems.filter((item) => item.status === "已启用").map((item) => item.id).sort();
assert.deepEqual(active, ["routing", "yijing-library", "zhouyi-benjing"].sort());
const zhouyi = systems.find((item) => item.id === "zhouyi-benjing");
assert.equal(zhouyi.grade, "S");
assert.match(zhouyi.basis, /周易/);
const qimen = systems.find((item) => item.id === "qimen");
assert.equal(qimen.status, "待校验");
assert.match(qimen.guardrail, /基准盘/);
console.log("system catalog verification passed");
FILE:agents/openai.yaml
interface:
display_name: "周易本经占筮"
short_description: "汲取百家之长,打磨成更完整的《周易》系统 | A more complete I Ching system distilled from the best of the field"
default_prompt: "Use $zhouyi-benjing-oracle to cast a Zhouyi hexagram or look up hexagram source text, cite the original judgment or line text first, and then explain it in plain Chinese."
FILE:package.json
{
"name": "zhouyi-benjing-oracle",
"version": "1.0.0",
"description": "以《周易》本经原著为底,系统收集并拆解市面上几乎所有可获取的同类 divination agents / skills / 程序,汲取百家之长,取其精华、去其糟粕,最终打磨成更准确、更完整、更好用的周易系统。Built on the original Zhouyi text and refined by absorbing the best ideas across virtually every comparable divination agent we could access.",
"main": "SKILL.md",
"keywords": [
"openclaw",
"skill",
"zhouyi",
"yijing",
"iching",
"周易",
"易经",
"六十四卦",
"占卜",
"卦辞",
"爻辞"
],
"author": "pineapple",
"license": "MIT-0",
"engines": {
"node": ">=18"
},
"scripts": {
"cast": "node scripts/zhouyi_cli.js cast",
"lookup": "node scripts/zhouyi_cli.js lookup",
"search": "node scripts/zhouyi_cli.js search",
"catalog": "node scripts/zhouyi_cli.js catalog",
"build:data": "python3 scripts/build_zhouyi_data.py",
"test": "node tests/verify_zhouyi_system.js && node tests/verify_system_catalog.js && node tests/verify_cli.js"
}
}
FILE:scripts/zhouyi_cli.js
#!/usr/bin/env node
const crypto = require("crypto");
const ZHOUYI_BENJING = require("../data/zhouyi-benjing");
const DIVINATION_SYSTEMS = require("../data/system-catalog");
const TRIGRAMS = {
"111": { name: "乾", symbol: "☰", nature: "天", image: "健" },
"110": { name: "兑", symbol: "☱", nature: "泽", image: "悦" },
"101": { name: "离", symbol: "☲", nature: "火", image: "丽" },
"100": { name: "震", symbol: "☳", nature: "雷", image: "动" },
"011": { name: "巽", symbol: "☴", nature: "风", image: "入" },
"010": { name: "坎", symbol: "☵", nature: "水", image: "险" },
"001": { name: "艮", symbol: "☶", nature: "山", image: "止" },
"000": { name: "坤", symbol: "☷", nature: "地", image: "顺" }
};
const HEXAGRAM_LOOKUP = {
"乾|乾": 1, "乾|兑": 43, "乾|离": 14, "乾|震": 34, "乾|巽": 9, "乾|坎": 5, "乾|艮": 26, "乾|坤": 11,
"兑|乾": 10, "兑|兑": 58, "兑|离": 38, "兑|震": 54, "兑|巽": 61, "兑|坎": 60, "兑|艮": 41, "兑|坤": 19,
"离|乾": 13, "离|兑": 49, "离|离": 30, "离|震": 55, "离|巽": 37, "离|坎": 63, "离|艮": 22, "离|坤": 36,
"震|乾": 25, "震|兑": 17, "震|离": 21, "震|震": 51, "震|巽": 42, "震|坎": 3, "震|艮": 27, "震|坤": 24,
"巽|乾": 44, "巽|兑": 28, "巽|离": 50, "巽|震": 32, "巽|巽": 57, "巽|坎": 48, "巽|艮": 18, "巽|坤": 46,
"坎|乾": 6, "坎|兑": 47, "坎|离": 64, "坎|震": 40, "坎|巽": 59, "坎|坎": 29, "坎|艮": 4, "坎|坤": 7,
"艮|乾": 33, "艮|兑": 31, "艮|离": 56, "艮|震": 62, "艮|巽": 53, "艮|坎": 39, "艮|艮": 52, "艮|坤": 15,
"坤|乾": 12, "坤|兑": 45, "坤|离": 35, "坤|震": 16, "坤|巽": 20, "坤|坎": 8, "坤|艮": 23, "坤|坤": 2
};
const POSITION_NAMES = ["初爻", "二爻", "三爻", "四爻", "五爻", "上爻"];
function usage() {
console.log(`
Usage:
node scripts/zhouyi_cli.js cast --question "..." --method coin|yarrow [--seed demo] [--json]
node scripts/zhouyi_cli.js lookup --name 乾 [--json]
node scripts/zhouyi_cli.js lookup --number 1 [--json]
node scripts/zhouyi_cli.js search --query "利涉大川" [--json]
node scripts/zhouyi_cli.js catalog [--grade S|B|C] [--query 关键词] [--json]
`);
}
function parseArgs(argv) {
const options = {};
for (let i = 0; i < argv.length; i += 1) {
const item = argv[i];
if (!item.startsWith("--")) continue;
const key = item.slice(2);
const next = argv[i + 1];
if (!next || next.startsWith("--")) {
options[key] = true;
} else {
options[key] = next;
i += 1;
}
}
return options;
}
function createSeededRng(seedInput) {
let seed = 0x811c9dc5;
for (const ch of String(seedInput)) {
seed ^= ch.charCodeAt(0);
seed = Math.imul(seed, 16777619);
}
if (!seed) seed = 0x9e3779b9;
return () => {
seed += 0x6d2b79f5;
let t = seed;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function randomRng() {
return () => {
if (typeof crypto.randomInt === "function") {
return crypto.randomInt(0, 1_000_000) / 1_000_000;
}
return Math.random();
};
}
function weightedPick(items, rng) {
const total = items.reduce((sum, item) => sum + item.weight, 0);
let cursor = rng() * total;
for (const item of items) {
cursor -= item.weight;
if (cursor < 0) return item.value;
}
return items[items.length - 1].value;
}
function createLine(value, coins = []) {
return {
value,
coins,
yang: value === 7 || value === 9,
moving: value === 6 || value === 9,
label: value === 6 ? "老阴" : value === 7 ? "少阳" : value === 8 ? "少阴" : "老阳"
};
}
function tossLine(method, rng) {
if (method === "yarrow") {
return createLine(
weightedPick(
[
{ value: 6, weight: 1 },
{ value: 7, weight: 5 },
{ value: 8, weight: 7 },
{ value: 9, weight: 3 }
],
rng
)
);
}
const coins = Array.from({ length: 3 }, () => (rng() < 0.5 ? 2 : 3));
return createLine(coins.reduce((sum, coin) => sum + coin, 0), coins);
}
function sameTrigramName(sourceName, trigramName) {
const aliases = { 兑: "兌", 离: "離" };
return sourceName === trigramName || sourceName === aliases[trigramName];
}
function fullHexagramName(name, upper, lower) {
if (upper.name === lower.name && sameTrigramName(name, upper.name)) {
return `name为upper.nature`;
}
return `upper.naturelower.naturename`;
}
function resolveHexagram(lines) {
const lowerBits = lines.slice(0, 3).map((line) => (line.yang ? "1" : "0")).join("");
const upperBits = lines.slice(3, 6).map((line) => (line.yang ? "1" : "0")).join("");
const lower = TRIGRAMS[lowerBits];
const upper = TRIGRAMS[upperBits];
const number = HEXAGRAM_LOOKUP[`lower.name|upper.name`];
const benjing = ZHOUYI_BENJING[number - 1];
return {
number,
name: benjing.name,
fullName: fullHexagramName(benjing.name, upper, lower),
judgment: benjing.judgment,
lines: benjing.lines,
extras: benjing.extras,
lower,
upper
};
}
function lineEntry(hexagram, index, priority) {
return {
title: `hexagram.namehexagram.lines[index].label`,
label: hexagram.lines[index].label,
text: hexagram.lines[index].text,
priority
};
}
function decideTextSource(primary, changed, moving) {
const count = moving.length;
if (count === 0) {
return {
rule: "六爻不变:以本卦卦辞为主。",
focus: "本卦卦辞",
entries: [{ title: `primary.name卦辞`, text: primary.judgment, priority: true }]
};
}
if (count === 1) {
const item = moving[0];
return {
rule: "一爻变:以该动爻爻辞为主。",
focus: primary.lines[item.index].label,
entries: [lineEntry(primary, item.index, true)]
};
}
if (count === 2) {
const indexes = moving.map((item) => item.index).sort((a, b) => a - b);
const upperIndex = indexes[indexes.length - 1];
return {
rule: "二爻变:取两条动爻爻辞,以上爻为主。",
focus: primary.lines[upperIndex].label,
entries: indexes.map((index) => lineEntry(primary, index, index === upperIndex))
};
}
if (count === 3) {
return {
rule: "三爻变:以本卦卦辞与变卦卦辞合看。",
focus: "本卦与变卦卦辞",
entries: [
{ title: `primary.name卦辞`, text: primary.judgment, priority: true },
{ title: `changed.name卦辞`, text: changed.judgment, priority: false }
]
};
}
if (count === 4) {
const staticIndexes = [0, 1, 2, 3, 4, 5].filter((index) => !moving.some((item) => item.index === index));
const lowerIndex = staticIndexes[0];
return {
rule: "四爻变:取两条静爻爻辞,以下爻为主。",
focus: primary.lines[lowerIndex].label,
entries: staticIndexes.map((index) => lineEntry(primary, index, index === lowerIndex))
};
}
if (count === 5) {
const staticIndex = [0, 1, 2, 3, 4, 5].find((index) => !moving.some((item) => item.index === index));
return {
rule: "五爻变:取变卦中唯一静爻所对应的爻辞。",
focus: changed.lines[staticIndex].label,
entries: [lineEntry(changed, staticIndex, true)]
};
}
const special =
primary.number === 1
? primary.extras.find((item) => item.label === "用九")
: primary.number === 2
? primary.extras.find((item) => item.label === "用六")
: null;
if (special) {
return {
rule: "六爻皆变:乾用用九,坤用用六。",
focus: special.label,
entries: [{ title: special.label, text: special.text, priority: true }]
};
}
return {
rule: "六爻皆变:乾坤之外,以变卦卦辞为主。",
focus: `changed.name卦辞`,
entries: [{ title: `changed.name卦辞`, text: changed.judgment, priority: true }]
};
}
function renderLineGlyph(line) {
if (line.yang) {
return line.moving ? "⚊ ○" : "⚊";
}
return line.moving ? "⚋ ×" : "⚋";
}
function castHexagram(question, method, seed) {
const rng = seed ? createSeededRng(seed) : randomRng();
const lines = Array.from({ length: 6 }, () => tossLine(method, rng));
const changedLines = lines.map((line) => ({
...line,
yang: line.moving ? !line.yang : line.yang,
moving: false
}));
const primary = resolveHexagram(lines);
const changed = resolveHexagram(changedLines);
const moving = lines
.map((line, index) => ({ index, line }))
.filter((item) => item.line.moving)
.map((item) => ({
index: item.index,
position: POSITION_NAMES[item.index],
originalLabel: primary.lines[item.index].label,
lineType: item.line.label,
text: primary.lines[item.index].text
}));
return {
question,
method,
seed: seed || null,
lines: lines.map((line, index) => ({
index,
position: POSITION_NAMES[index],
lineType: line.label,
value: line.value,
moving: line.moving,
yang: line.yang,
coins: line.coins,
glyph: renderLineGlyph(line)
})),
primary,
changed,
moving,
decision: decideTextSource(primary, changed, moving)
};
}
function lookupHexagramByName(name) {
return ZHOUYI_BENJING.find((item) => item.name === name) || null;
}
function lookupHexagramByNumber(number) {
const value = Number(number);
if (!Number.isInteger(value) || value < 1 || value > 64) return null;
return ZHOUYI_BENJING[value - 1];
}
function searchHexagrams(query) {
const keyword = String(query || "").trim();
return ZHOUYI_BENJING.filter((item) => {
if (item.name.includes(keyword) || item.judgment.includes(keyword)) return true;
if (item.lines.some((line) => line.text.includes(keyword) || line.label.includes(keyword))) return true;
if (item.extras.some((extra) => extra.text.includes(keyword) || extra.label.includes(keyword))) return true;
return false;
}).map((item) => ({
number: item.number,
name: item.name,
judgment: item.judgment
}));
}
function filterCatalog(query, grade) {
return DIVINATION_SYSTEMS.filter((item) => {
const gradeOk = !grade || grade === "all" || item.grade === grade;
if (!gradeOk) return false;
if (!query) return true;
const haystack = [
item.name,
item.family,
item.basis,
item.capability,
item.guardrail,
...(item.bestFor || []),
...(item.inputs || [])
].join(" ");
return haystack.includes(query);
});
}
function print(payload, asJson, formatter) {
if (asJson) {
console.log(JSON.stringify(payload, null, 2));
return;
}
console.log(formatter(payload));
}
function formatCast(payload) {
const lines = payload.lines
.slice()
.reverse()
.map((line) => `line.position line.glyph line.lineType`)
.join("\n");
const selected = payload.decision.entries
.map((entry) => `entry.title""\nentry.text`)
.join("\n\n");
return [
`问题:payload.question || "未填写"`,
`方法:payload.method`,
`本卦:payload.primary.number. payload.primary.fullName`,
`变卦:payload.changed.number. payload.changed.fullName`,
`动爻:"无"`,
"",
lines,
"",
`取辞规则:payload.decision.rule`,
selected
].join("\n");
}
function formatLookup(payload) {
const lines = payload.lines.map((line) => `line.label:line.text`).join("\n");
const extras = payload.extras.length ? `\npayload.extras.map((item) => `${item.label:item.text`).join("\n")}` : "";
return `payload.number. payload.name\n卦辞:payload.judgment\nlinesextras`;
}
function formatSearch(results) {
if (!results.length) return "未找到匹配卦象。";
return results.map((item) => `item.number. item.name:item.judgment`).join("\n");
}
function formatCatalog(results) {
if (!results.length) return "未找到匹配体系。";
return results
.map((item) => `item.grade item.name [item.status]\n依据:item.basis\n能力:item.capability\n边界:item.guardrail`)
.join("\n\n");
}
function main() {
const command = process.argv[2];
const options = parseArgs(process.argv.slice(3));
const asJson = Boolean(options.json || options.format === "json");
if (!command || command === "help" || command === "--help" || command === "-h") {
usage();
process.exit(command ? 0 : 1);
}
if (command === "cast") {
const method = options.method === "yarrow" ? "yarrow" : "coin";
const payload = castHexagram(options.question || "", method, options.seed || "");
print(payload, asJson, formatCast);
return;
}
if (command === "lookup") {
const result = options.name ? lookupHexagramByName(options.name) : lookupHexagramByNumber(options.number);
if (!result) {
console.error("未找到指定卦象。请提供 --name 或 --number。");
process.exit(1);
}
print(result, asJson, formatLookup);
return;
}
if (command === "search") {
if (!options.query) {
console.error("search 需要 --query。");
process.exit(1);
}
const results = searchHexagrams(options.query);
print(results, asJson, formatSearch);
return;
}
if (command === "catalog") {
const results = filterCatalog(options.query || "", options.grade || "all");
print(results, asJson, formatCatalog);
return;
}
console.error(`未知命令:command`);
usage();
process.exit(1);
}
main();
FILE:scripts/publish.sh
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
DIST_DIR="$(cd "$PROJECT_DIR/.." && pwd)/dist"
VERSION=""
DISPLAY_NAME=""
usage() {
echo "Usage: $(basename "$0") --version <semver> [--name <display-name>]"
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
VERSION="-"
shift 2
;;
--name)
DISPLAY_NAME="-"
shift 2
;;
*)
usage
;;
esac
done
[[ -n "$VERSION" ]] || usage
python3 - <<PY
import json
from pathlib import Path
project = Path(r"$PROJECT_DIR")
version = "$VERSION"
for filename in ("package.json", "_meta.json"):
path = project / filename
data = json.loads(path.read_text(encoding="utf-8"))
data["version"] = version
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
skill_path = project / "SKILL.md"
text = skill_path.read_text(encoding="utf-8")
if "version: " in text:
import re
text = re.sub(
r'(^\s*version:\s*")[^"]+(")',
lambda m: f'{m.group(1)}{version}{m.group(2)}',
text,
flags=re.M,
)
skill_path.write_text(text, encoding="utf-8")
PY
mkdir -p "$DIST_DIR"
ZIP_PATH="$DIST_DIR/zhouyi-benjing-oracle-$VERSION.zip"
rm -f "$ZIP_PATH"
cd "$(dirname "$PROJECT_DIR")"
zip -rq "$ZIP_PATH" "$(basename "$PROJECT_DIR")" -x "*/__pycache__/*" "*/.DS_Store"
echo "Built $ZIP_PATH"
echo ""
echo "ClawHub CLI publish command:"
if [[ -n "$DISPLAY_NAME" ]]; then
echo "clawhub publish \"$PROJECT_DIR\" --version \"$VERSION\" --name \"$DISPLAY_NAME\""
else
echo "clawhub publish \"$PROJECT_DIR\" --version \"$VERSION\""
fi
FILE:scripts/build_zhouyi_data.py
#!/usr/bin/env python3
"""Build a browser-loadable Zhouyi Benjing data file from the local source text."""
from __future__ import annotations
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "references" / "zhouyi-benjing-source.txt"
OUTPUT = ROOT / "data" / "zhouyi-benjing.js"
LINE_LABELS = ("初九", "初六", "九二", "六二", "九三", "六三", "九四", "六四", "九五", "六五", "上九", "上六")
EXTRA_LABELS = ("用九", "用六")
def compact(value: str) -> str:
return re.sub(r"\s+", "", value.strip())
def parse_source(text: str) -> list[dict]:
lines = [line.strip() for line in text.splitlines()]
hexagrams: list[dict] = []
i = 0
while i < len(lines):
if not re.match(r"^第\s*\S+\s*卦$", lines[i]):
i += 1
continue
number = len(hexagrams) + 1
i += 1
while i < len(lines) and not lines[i]:
i += 1
name_line = lines[i]
inline_judgment = ""
inline_match = re.match(r"^([^:]+):(.+)$", name_line)
if inline_match:
name = inline_match.group(1).strip()
inline_judgment = inline_match.group(2).strip()
else:
name = name_line
i += 1
while i < len(lines) and not lines[i]:
i += 1
judgment_parts: list[str] = [inline_judgment] if inline_judgment else []
yao_lines: list[dict] = []
extra_lines: list[dict] = []
while i < len(lines):
line = lines[i]
if re.match(r"^第\s*\S+\s*卦$", line):
break
i += 1
if not line:
continue
match = re.match(r"^([^:]+):(.+)$", line)
if not match:
if judgment_parts and not yao_lines and not extra_lines:
judgment_parts.append(line)
continue
label, text_part = match.group(1), match.group(2)
if label in LINE_LABELS:
yao_lines.append({"label": label, "text": text_part.strip()})
elif label in EXTRA_LABELS:
extra_lines.append({"label": label, "text": text_part.strip()})
elif compact(label) == compact(name):
judgment_parts.append(text_part.strip())
elif judgment_parts and not yao_lines and not extra_lines:
judgment_parts.append(line)
hexagrams.append(
{
"number": number,
"name": name,
"judgment": "".join(judgment_parts),
"lines": yao_lines,
"extras": extra_lines,
}
)
return hexagrams
def validate(hexagrams: list[dict]) -> None:
if len(hexagrams) != 64:
raise SystemExit(f"expected 64 hexagrams, got {len(hexagrams)}")
for item in hexagrams:
if not item["judgment"]:
raise SystemExit(f"hexagram {item['number']} {item['name']} has no judgment")
if len(item["lines"]) != 6:
raise SystemExit(
f"hexagram {item['number']} {item['name']} expected 6 lines, got {len(item['lines'])}"
)
if hexagrams[0]["name"] != "乾" or hexagrams[0]["extras"][0]["label"] != "用九":
raise SystemExit("乾卦 or 用九 parse failed")
if hexagrams[1]["name"] != "坤" or hexagrams[1]["extras"][0]["label"] != "用六":
raise SystemExit("坤卦 or 用六 parse failed")
if hexagrams[2]["lines"][1]["text"] != "屯如邅如,乘馬班如。匪寇婚媾,女子貞不字,十年乃字。":
raise SystemExit("屯六二 source text was not preserved exactly")
def main() -> None:
text = SOURCE.read_text(encoding="utf-8")
hexagrams = parse_source(text)
validate(hexagrams)
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
payload = json.dumps(hexagrams, ensure_ascii=False, indent=2)
OUTPUT.write_text(
"const ZHOUYI_BENJING = "
+ payload
+ ";\n\n"
+ "if (typeof window !== \"undefined\") window.ZHOUYI_BENJING = ZHOUYI_BENJING;\n"
+ "if (typeof module !== \"undefined\") module.exports = ZHOUYI_BENJING;\n",
encoding="utf-8",
)
print(f"wrote {OUTPUT.relative_to(ROOT)} with {len(hexagrams)} hexagrams")
if __name__ == "__main__":
main()
FILE:data/system-catalog.js
const DIVINATION_SYSTEMS = [
{
id: "zhouyi-benjing",
name: "周易本经占筮",
family: "易",
grade: "S",
status: "已启用",
basis: "《周易》本经卦辞、爻辞、用九、用六",
bestFor: ["当下决策", "时机判断", "关系与事业抉择", "自我复盘"],
inputs: ["一个明确问题", "起卦方式"],
capability: "可起卦、取辞、引用本经原文并给出现代解释。",
guardrail: "只作解释性参考,不替代专业判断。"
},
{
id: "yijing-library",
name: "六十四卦本经库",
family: "易",
grade: "S",
status: "已启用",
basis: "本地 Gutenberg《易經》整理文本",
bestFor: ["查卦辞", "查爻辞", "校验卦序", "学习原文"],
inputs: ["卦名、卦序或关键词"],
capability: "可检索六十四卦卦辞和六爻原文。",
guardrail: "当前只含本经,不混入彖象传和后世注解。"
},
{
id: "meihua",
name: "梅花易数",
family: "易象",
grade: "B",
status: "知识库",
basis: "以卦象、体用、生克、动爻为主的后世易占体系",
bestFor: ["快速起象", "当下气机", "小事趋势"],
inputs: ["数、时间、物象或随机触发"],
capability: "当前只提供体系说明和路由建议,暂不自动断梅花盘。",
guardrail: "避免把数字取卦与《周易》本经取辞混为一谈。"
},
{
id: "liuyao",
name: "六爻纳甲",
family: "易占",
grade: "B",
status: "待校验",
basis: "纳甲、六亲、世应、月建日辰、动变生克",
bestFor: ["具体事项成败", "失物", "官司", "短期应期"],
inputs: ["六爻结果", "起卦时间", "明确用神"],
capability: "暂不开放自动断卦,只保留未来高精度实现入口。",
guardrail: "必须先校验纳甲、世应、六亲、月日旺衰,否则不输出结论。"
},
{
id: "xiaoliuren",
name: "小六壬",
family: "民间时占",
grade: "C",
status: "知识库",
basis: "大安、留连、速喜、赤口、小吉、空亡六神课",
bestFor: ["轻量日常问事", "出门前判断", "粗略吉凶"],
inputs: ["月、日、时或指定数字"],
capability: "当前只解释体系差异,不作为周易本经断语依据。",
guardrail: "小六壬不是《周易》原文体系,不能冒充易经本经。"
},
{
id: "qimen",
name: "奇门遁甲",
family: "三式",
grade: "B",
status: "待校验",
basis: "节气、阴阳遁、局数、九星八门八神、三奇六仪",
bestFor: ["择时", "方位", "项目推进窗口", "行动策略"],
inputs: ["精确时间", "地点/时区", "问事类别"],
capability: "只纳入百科与路由,不使用未校验日柱和定局算法。",
guardrail: "四柱、节气、值符值使必须通过基准盘测试后才能开放排盘。"
},
{
id: "bazi",
name: "八字四柱",
family: "命理",
grade: "B",
status: "待校验",
basis: "年、月、日、时四柱与十神、旺衰、大运",
bestFor: ["长期人格底色", "阶段节奏", "职业倾向"],
inputs: ["出生年月日时", "出生地", "性别/历法说明"],
capability: "当前只记录资料要求和解释边界。",
guardrail: "必须使用可靠历法库和流派说明,不能用简化干支表替代。"
},
{
id: "ziwei",
name: "紫微斗数",
family: "命理",
grade: "B",
status: "待校验",
basis: "农历生日、时辰、命宫身宫、十四主星、辅煞、四化",
bestFor: ["人生阶段", "十二宫主题", "关系与事业结构"],
inputs: ["农历/公历生日", "时辰", "性别", "历法转换规则"],
capability: "当前不自动排盘,避免命宫、五行局、安星错误。",
guardrail: "必须通过成熟排盘库交叉验证后才输出命盘解释。"
},
{
id: "fengshui",
name: "风水与九宫飞星",
family: "环境",
grade: "C",
status: "知识库",
basis: "空间方位、流年飞星、住宅格局与现实动线",
bestFor: ["居住调整", "办公布局", "空间复盘"],
inputs: ["户型图", "朝向", "入住时间", "实际使用方式"],
capability: "只提供知识框架,不用缺资料的脚本给方位断语。",
guardrail: "没有户型和朝向时,只能做一般环境建议。"
},
{
id: "tarot",
name: "塔罗",
family: "西方象征",
grade: "C",
status: "知识库",
basis: "牌阵、牌义、关系与短期趋势象征",
bestFor: ["心理镜像", "关系觉察", "短期选择题"],
inputs: ["牌阵", "抽牌结果", "问题"],
capability: "作为百科补充,不参与周易本经结论。",
guardrail: "塔罗不是周易体系,不能与本经原文互相冒充依据。"
},
{
id: "astrology",
name: "西方星盘",
family: "西方占星",
grade: "C",
status: "知识库",
basis: "出生星图、行运、合盘",
bestFor: ["人格模式", "关系合盘", "阶段性主题"],
inputs: ["出生年月日时", "出生地"],
capability: "当前只作为百科条目和未来扩展方向。",
guardrail: "需要精确天文计算库,不使用泛星座替代星盘。"
},
{
id: "routing",
name: "综合问事路由",
family: "产品层",
grade: "S",
status: "已启用",
basis: "问题类型、资料完整度、体系可信度",
bestFor: ["选择体系", "明确资料缺口", "避免误用"],
inputs: ["用户问题", "已有资料"],
capability: "帮助判断该用本经占筮、查卦库,还是等待更完整资料。",
guardrail: "宁可降级说明,也不冒充高精度。"
}
];
if (typeof window !== "undefined") window.DIVINATION_SYSTEMS = DIVINATION_SYSTEMS;
if (typeof module !== "undefined") module.exports = DIVINATION_SYSTEMS;
FILE:data/zhouyi-benjing.js
const ZHOUYI_BENJING = [
{
"number": 1,
"name": "乾",
"judgment": "元,亨,利,貞。",
"lines": [
{
"label": "初九",
"text": "潛龍,勿用。"
},
{
"label": "九二",
"text": "見龍在田,利見大人。"
},
{
"label": "九三",
"text": "君子終日乾乾,夕惕,若厲,無咎。"
},
{
"label": "九四",
"text": "或躍在淵,無咎。"
},
{
"label": "九五",
"text": "飛龍在天,利見大人。"
},
{
"label": "上九",
"text": "亢龍有悔。"
}
],
"extras": [
{
"label": "用九",
"text": "見群龍無首,吉。"
}
]
},
{
"number": 2,
"name": "坤",
"judgment": "元,亨,利牝馬之貞。君子有攸往,先迷後得主,利西南得朋,東北喪朋。安貞,吉。",
"lines": [
{
"label": "初六",
"text": "履霜,堅冰至。"
},
{
"label": "六二",
"text": "直,方,大,不習無不利。"
},
{
"label": "六三",
"text": "含章可貞。或從王事,無成有終。"
},
{
"label": "六四",
"text": "括囊;無咎,無譽。"
},
{
"label": "六五",
"text": "黃裳,元吉。"
},
{
"label": "上六",
"text": "戰龍於野,其血玄黃。"
}
],
"extras": [
{
"label": "用六",
"text": "利永貞。"
}
]
},
{
"number": 3,
"name": "屯",
"judgment": "元,亨,利,貞,勿用,有攸往,利建侯。",
"lines": [
{
"label": "初九",
"text": "磐桓;利居貞,利建侯。"
},
{
"label": "六二",
"text": "屯如邅如,乘馬班如。匪寇婚媾,女子貞不字,十年乃字。"
},
{
"label": "六三",
"text": "既鹿無虞,惟入于林中,君子幾不如舍,往吝。"
},
{
"label": "六四",
"text": "乘馬班如,求婚媾,往吉,無不利。"
},
{
"label": "九五",
"text": "屯其膏,小貞吉,大貞凶。"
},
{
"label": "上六",
"text": "乘馬班如,泣血漣如。"
}
],
"extras": []
},
{
"number": 4,
"name": "蒙",
"judgment": "亨。匪我求童蒙,童蒙求我。初噬告,再三瀆,瀆則不告。利貞。",
"lines": [
{
"label": "初六",
"text": "發蒙,利用刑人,用說桎梏,以往吝。"
},
{
"label": "九二",
"text": "包蒙,吉;納婦,吉;子克家。"
},
{
"label": "六三",
"text": "勿用娶女;見金夫,不有躬,無攸利。"
},
{
"label": "六四",
"text": "困蒙,吝。"
},
{
"label": "六五",
"text": "童蒙,吉。"
},
{
"label": "上九",
"text": "擊蒙;不利為寇,利御寇。"
}
],
"extras": []
},
{
"number": 5,
"name": "需",
"judgment": "有孚,光亨,貞吉。利涉大川。",
"lines": [
{
"label": "初九",
"text": "需于郊。利用恆,無咎。"
},
{
"label": "九二",
"text": "需于沙。小有言,終吉。"
},
{
"label": "九三",
"text": "需于泥,致寇至。"
},
{
"label": "六四",
"text": "需于血,出自穴。"
},
{
"label": "九五",
"text": "需于酒食,貞吉。"
},
{
"label": "上六",
"text": "入于穴,有不速之客三人來,敬之終吉。"
}
],
"extras": []
},
{
"number": 6,
"name": "訟",
"judgment": "有孚,窒。惕中吉。終凶。利見大人,不利涉大川。",
"lines": [
{
"label": "初六",
"text": "不永所事,小有言,終吉。"
},
{
"label": "九二",
"text": "不克訟,歸而逋,其邑人三百戶,無眚。"
},
{
"label": "六三",
"text": "食舊德,貞厲,終吉,或從王事,無成。"
},
{
"label": "九四",
"text": "不克訟,復即命,渝安貞,吉。"
},
{
"label": "九五",
"text": "訟元吉。"
},
{
"label": "上九",
"text": "或錫之鞶帶,終朝三褫之。"
}
],
"extras": []
},
{
"number": 7,
"name": "師",
"judgment": "貞,丈人,吉無咎。",
"lines": [
{
"label": "初六",
"text": "師出以律,否臧凶。"
},
{
"label": "九二",
"text": "在師中,吉無咎,王三錫命。"
},
{
"label": "六三",
"text": "師或輿尸,凶。"
},
{
"label": "六四",
"text": "師左次,無咎。"
},
{
"label": "六五",
"text": "田有禽,利執言,無咎。長子帥師,弟子輿尸,貞凶。"
},
{
"label": "上六",
"text": "大君有命,開國承家,小人勿用。"
}
],
"extras": []
},
{
"number": 8,
"name": "比",
"judgment": "吉。原筮元永貞,無咎。不寧方來,後夫凶。",
"lines": [
{
"label": "初六",
"text": "有孚比之,無咎。有孚盈缶,終來有他,吉。"
},
{
"label": "六二",
"text": "比之自內,貞吉。"
},
{
"label": "六三",
"text": "比之匪人。"
},
{
"label": "六四",
"text": "外比之,貞吉。"
},
{
"label": "九五",
"text": "顯比,王用三驅,失前禽。邑人不誡,吉。"
},
{
"label": "上六",
"text": "比之無首,凶。"
}
],
"extras": []
},
{
"number": 9,
"name": "小畜",
"judgment": "亨。密雲不雨,自我西郊。",
"lines": [
{
"label": "初九",
"text": "復自道,何其咎,吉。"
},
{
"label": "九二",
"text": "牽復,吉。"
},
{
"label": "九三",
"text": "輿說輻,夫妻反目。"
},
{
"label": "六四",
"text": "有孚,血去。惕出,無咎。"
},
{
"label": "九五",
"text": "有孚攣如,富以其鄰。"
},
{
"label": "上九",
"text": "既雨既處,尚德載婦,貞厲。月幾望,君子征凶。"
}
],
"extras": []
},
{
"number": 10,
"name": "履",
"judgment": "履虎尾,不咥人,亨。",
"lines": [
{
"label": "初九",
"text": "素履,往無咎。"
},
{
"label": "九二",
"text": "履道坦坦,幽人貞吉。"
},
{
"label": "六三",
"text": "眇能視,跛能履,履虎尾,咥人,凶。武人為于大君。"
},
{
"label": "九四",
"text": "履虎尾,愬愬,終吉。"
},
{
"label": "九五",
"text": "夬履,貞厲。"
},
{
"label": "上九",
"text": "視履考祥,其旋元吉。"
}
],
"extras": []
},
{
"number": 11,
"name": "泰",
"judgment": "小往大來,吉亨。",
"lines": [
{
"label": "初九",
"text": "拔茅茹,以其彙,征吉。"
},
{
"label": "九二",
"text": "包荒,用馮河,不遐遺,朋亡,得尚于中行。"
},
{
"label": "九三",
"text": "無平不陂,無往不復,艱貞無咎。勿恤其孚,于食有福。"
},
{
"label": "六四",
"text": "翩翩不富,以其鄰,不戒以孚。"
},
{
"label": "六五",
"text": "帝乙歸妹,以祉元吉。"
},
{
"label": "上六",
"text": "城復于隍,勿用師。自邑告命,貞吝。"
}
],
"extras": []
},
{
"number": 12,
"name": "否",
"judgment": "否之匪人,不利君子貞,大往小來。",
"lines": [
{
"label": "初六",
"text": "拔茅茹,以其彙,貞吉亨。"
},
{
"label": "六二",
"text": "包承。小人吉,大人否亨。"
},
{
"label": "六三",
"text": "包羞。"
},
{
"label": "九四",
"text": "有命無咎,疇離祉。"
},
{
"label": "九五",
"text": "休否,大人吉。其亡其亡,繫于苞桑。"
},
{
"label": "上九",
"text": "傾否,先否後喜。"
}
],
"extras": []
},
{
"number": 13,
"name": "同人",
"judgment": "同人于野,亨。利涉大川,利君子貞。",
"lines": [
{
"label": "初九",
"text": "同人于門,無咎。"
},
{
"label": "六二",
"text": "同人于宗,吝。"
},
{
"label": "九三",
"text": "伏戎于莽,升其高陵,三歲不興。"
},
{
"label": "九四",
"text": "乘其墉,弗克攻,吉。"
},
{
"label": "九五",
"text": "同人,先號咷而後笑。大師克相遇。"
},
{
"label": "上九",
"text": "同人于郊,無悔。"
}
],
"extras": []
},
{
"number": 14,
"name": "大有",
"judgment": "元亨。",
"lines": [
{
"label": "初九",
"text": "無交害,匪咎,艱則無咎。"
},
{
"label": "九二",
"text": "大車以載,有攸往,無咎。"
},
{
"label": "九三",
"text": "公用亨于天子,小人弗克。"
},
{
"label": "九四",
"text": "匪其彭,無咎。"
},
{
"label": "六五",
"text": "厥孚交如,威如;吉。"
},
{
"label": "上九",
"text": "自天佑之,吉無不利。"
}
],
"extras": []
},
{
"number": 15,
"name": "謙",
"judgment": "亨,君子有終。",
"lines": [
{
"label": "初六",
"text": "謙謙君子,用涉大川,吉。"
},
{
"label": "六二",
"text": "鳴謙,貞吉。"
},
{
"label": "九三",
"text": "勞謙君子,有終吉。"
},
{
"label": "六四",
"text": "無不利,撝謙。"
},
{
"label": "六五",
"text": "不富,以其鄰,利用侵伐,無不利。"
},
{
"label": "上六",
"text": "鳴謙,利用行師,征邑國。"
}
],
"extras": []
},
{
"number": 16,
"name": "豫",
"judgment": "利建侯行師。",
"lines": [
{
"label": "初六",
"text": "鳴豫,凶。"
},
{
"label": "六二",
"text": "介于石,不終日,貞吉。"
},
{
"label": "六三",
"text": "盱豫,悔。遲有悔。"
},
{
"label": "九四",
"text": "由豫,大有得。勿疑。朋盍簪。"
},
{
"label": "六五",
"text": "貞疾,恆不死。"
},
{
"label": "上六",
"text": "冥豫,成有渝,無咎。"
}
],
"extras": []
},
{
"number": 17,
"name": "隨",
"judgment": "元亨利貞,無咎。",
"lines": [
{
"label": "初九",
"text": "官有渝,貞吉。出門交有功。"
},
{
"label": "六二",
"text": "係小子,失丈夫。"
},
{
"label": "六三",
"text": "係丈夫,失小子。隨有求得,利居貞。"
},
{
"label": "九四",
"text": "隨有獲,貞凶。有孚在道,以明,何咎。"
},
{
"label": "九五",
"text": "孚于嘉,吉。"
},
{
"label": "上六",
"text": "拘系之,乃從維之。王用亨于西山。"
}
],
"extras": []
},
{
"number": 18,
"name": "蠱",
"judgment": "元亨,利涉大川。先甲三日,後甲三日。",
"lines": [
{
"label": "初六",
"text": "幹父之蠱,有子,考無咎,厲終吉。"
},
{
"label": "九二",
"text": "幹母之蠱,不可貞。"
},
{
"label": "九三",
"text": "幹父小有晦,無大咎。"
},
{
"label": "六四",
"text": "裕父之蠱,往見吝。"
},
{
"label": "六五",
"text": "幹父之蠱,用譽。"
},
{
"label": "上九",
"text": "不事王侯,高尚其事。"
}
],
"extras": []
},
{
"number": 19,
"name": "臨",
"judgment": "元,亨,利,貞。至于八月有凶。",
"lines": [
{
"label": "初九",
"text": "咸臨,貞吉。"
},
{
"label": "九二",
"text": "咸臨,吉無不利。"
},
{
"label": "六三",
"text": "甘臨,無攸利。既憂之,無咎。"
},
{
"label": "六四",
"text": "至臨,無咎。"
},
{
"label": "六五",
"text": "知臨,大君之宜,吉。"
},
{
"label": "上六",
"text": "敦臨,吉無咎。"
}
],
"extras": []
},
{
"number": 20,
"name": "觀",
"judgment": "盥而不薦,有孚顒若。",
"lines": [
{
"label": "初六",
"text": "童觀,小人無咎,君子吝。"
},
{
"label": "六二",
"text": "窺觀,利女貞。"
},
{
"label": "六三",
"text": "觀我生,進退。"
},
{
"label": "六四",
"text": "觀國之光,利用賓于王。"
},
{
"label": "九五",
"text": "觀我生,君子無咎。"
},
{
"label": "上九",
"text": "觀其生,君子無咎。"
}
],
"extras": []
},
{
"number": 21,
"name": "噬嗑",
"judgment": "亨。利用獄。",
"lines": [
{
"label": "初九",
"text": "履校滅趾,無咎。"
},
{
"label": "六二",
"text": "噬膚滅鼻,無咎。"
},
{
"label": "六三",
"text": "噬臘肉,遇毒;小吝,無咎。"
},
{
"label": "九四",
"text": "噬乾胏,得金矢,利艱貞,吉。"
},
{
"label": "六五",
"text": "噬乾肉,得黃金,貞厲,無咎。"
},
{
"label": "上九",
"text": "何校滅耳,凶。"
}
],
"extras": []
},
{
"number": 22,
"name": "賁",
"judgment": "亨。小利有所往。",
"lines": [
{
"label": "初九",
"text": "賁其趾,舍車而徒。"
},
{
"label": "六二",
"text": "賁其須。"
},
{
"label": "九三",
"text": "賁如濡如,永貞吉。"
},
{
"label": "六四",
"text": "賁如皤如,白馬翰如,匪寇婚媾。"
},
{
"label": "六五",
"text": "賁于丘園,束帛戔戔,吝,終吉。"
},
{
"label": "上九",
"text": "白賁,無咎。"
}
],
"extras": []
},
{
"number": 23,
"name": "剝",
"judgment": "不利有攸往。",
"lines": [
{
"label": "初六",
"text": "剝床以足,蔑貞凶。"
},
{
"label": "六二",
"text": "剝床以辨,蔑貞凶。"
},
{
"label": "六三",
"text": "剝之,無咎。"
},
{
"label": "六四",
"text": "剝床以膚,凶。"
},
{
"label": "六五",
"text": "貫魚,以宮人寵,無不利。"
},
{
"label": "上九",
"text": "碩果不食,君子得輿,小人剝廬。"
}
],
"extras": []
},
{
"number": 24,
"name": "復",
"judgment": "亨。出入無疾,朋來無咎。反復其道,七日來復,利有攸往。",
"lines": [
{
"label": "初九",
"text": "不復遠,無祗悔,元吉。"
},
{
"label": "六二",
"text": "休復,吉。"
},
{
"label": "六三",
"text": "頻復厲,無咎。"
},
{
"label": "六四",
"text": "中行獨復。"
},
{
"label": "六五",
"text": "敦復,無悔。"
},
{
"label": "上六",
"text": "迷復,凶,有災眚。用行師,終有大敗,以其國君,凶;至于十年,不克征。"
}
],
"extras": []
},
{
"number": 25,
"name": "無妄",
"judgment": "元,亨,利,貞。其匪正有眚,不利有攸往。",
"lines": [
{
"label": "初九",
"text": "無妄,往吉。"
},
{
"label": "六二",
"text": "不耕獲,不菑畬,則利有攸往。"
},
{
"label": "六三",
"text": "無妄之災,或繫之牛,行人之得,邑人之災。"
},
{
"label": "九四",
"text": "可貞,無咎。"
},
{
"label": "九五",
"text": "無妄之疾,勿藥有喜。"
},
{
"label": "上九",
"text": "無妄,行有眚,無攸利。"
}
],
"extras": []
},
{
"number": 26,
"name": "大畜",
"judgment": "利貞,不家食吉,利涉大川。",
"lines": [
{
"label": "初九",
"text": "有厲利已。"
},
{
"label": "九二",
"text": "輿說輻。"
},
{
"label": "九三",
"text": "良馬逐,利艱貞。日閑輿衛,利有攸往。"
},
{
"label": "六四",
"text": "童牛之牿,元吉。"
},
{
"label": "六五",
"text": "豶豕之牙,吉。"
},
{
"label": "上九",
"text": "何天之衢,亨。"
}
],
"extras": []
},
{
"number": 27,
"name": "頤",
"judgment": "貞吉。觀頤,自求口實。",
"lines": [
{
"label": "初九",
"text": "舍爾靈龜,觀我朵頤,凶。"
},
{
"label": "六二",
"text": "顛頤,拂經于丘頤,征凶。"
},
{
"label": "六三",
"text": "拂頤,貞凶,十年勿用,無攸利。"
},
{
"label": "六四",
"text": "顛頤吉,虎視眈眈,其欲逐逐,無咎。"
},
{
"label": "六五",
"text": "拂經,居貞吉,不可涉大川。"
},
{
"label": "上九",
"text": "由頤,厲吉,利涉大川。"
}
],
"extras": []
},
{
"number": 28,
"name": "大過",
"judgment": "棟橈,利有攸往,亨。",
"lines": [
{
"label": "初六",
"text": "藉用白茅,無咎。"
},
{
"label": "九二",
"text": "枯楊生稊,老夫得其女妻,無不利。"
},
{
"label": "九三",
"text": "棟橈,凶。"
},
{
"label": "九四",
"text": "棟隆,吉;有它吝。"
},
{
"label": "九五",
"text": "枯楊生華,老婦得士夫,無咎無譽。"
},
{
"label": "上六",
"text": "過涉滅頂,凶,無咎。"
}
],
"extras": []
},
{
"number": 29,
"name": "坎",
"judgment": "習坎,有孚,維心亨,行有尚。",
"lines": [
{
"label": "初六",
"text": "習坎,入于坎窞,凶。"
},
{
"label": "九二",
"text": "坎有險,求小得。"
},
{
"label": "六三",
"text": "來之坎坎,險且枕,入于坎窞,勿用。"
},
{
"label": "六四",
"text": "樽酒簋貳,用缶,納約自牖,終無咎。"
},
{
"label": "九五",
"text": "坎不盈,祗既平,無咎。"
},
{
"label": "上六",
"text": "係用徽纆,寘于叢棘,三歲不得,凶。"
}
],
"extras": []
},
{
"number": 30,
"name": "離",
"judgment": "利貞,亨。畜牝牛,吉。",
"lines": [
{
"label": "初九",
"text": "履錯然,敬之無咎。"
},
{
"label": "六二",
"text": "黃離,元吉。"
},
{
"label": "九三",
"text": "日昃之離,不鼓缶而歌,則大耋之嗟,凶。"
},
{
"label": "九四",
"text": "突如其來如,焚如,死如,棄如。"
},
{
"label": "六五",
"text": "出涕沱若,戚嗟若,吉。"
},
{
"label": "上九",
"text": "王用出征,有嘉折首,獲匪其醜,無咎。"
}
],
"extras": []
},
{
"number": 31,
"name": "咸",
"judgment": "亨,利貞,取女吉。",
"lines": [
{
"label": "初六",
"text": "咸其拇。"
},
{
"label": "六二",
"text": "咸其腓,凶,居吉。"
},
{
"label": "九三",
"text": "咸其股,執其隨,往吝。"
},
{
"label": "九四",
"text": "貞吉悔亡,憧憧往來,朋從爾思。"
},
{
"label": "九五",
"text": "咸其脢,無悔。"
},
{
"label": "上六",
"text": "咸其輔,頰,舌。"
}
],
"extras": []
},
{
"number": 32,
"name": "恆",
"judgment": "亨,無咎,利貞,利有攸往。",
"lines": [
{
"label": "初六",
"text": "浚恆,貞凶,無攸利。"
},
{
"label": "九二",
"text": "悔亡。"
},
{
"label": "九三",
"text": "不恆其德,或承之羞,貞吝。"
},
{
"label": "九四",
"text": "田無禽。"
},
{
"label": "六五",
"text": "恆其德,貞,婦人吉,夫子凶。"
},
{
"label": "上六",
"text": "振恆,凶。"
}
],
"extras": []
},
{
"number": 33,
"name": "遯",
"judgment": "亨,小利貞。",
"lines": [
{
"label": "初六",
"text": "遯尾,厲,勿用有攸往。"
},
{
"label": "六二",
"text": "執之用黃牛之革,莫之勝說。"
},
{
"label": "九三",
"text": "係遯,有疾厲,畜臣妾吉。"
},
{
"label": "九四",
"text": "好遯君子吉,小人否。"
},
{
"label": "九五",
"text": "嘉遯,貞吉。"
},
{
"label": "上九",
"text": "肥遯,無不利。"
}
],
"extras": []
},
{
"number": 34,
"name": "大壯",
"judgment": "利貞。",
"lines": [
{
"label": "初九",
"text": "壯于趾,征凶,有孚。"
},
{
"label": "九二",
"text": "貞吉。"
},
{
"label": "九三",
"text": "小人用壯,君子用罔,貞厲。羝羊觸藩,羸其角。"
},
{
"label": "九四",
"text": "貞吉悔亡,藩決不羸,壯于大輿之輹。"
},
{
"label": "六五",
"text": "喪羊于易,無悔。"
},
{
"label": "上六",
"text": "羝羊觸藩,不能退,不能遂,無攸利,艱則吉。"
}
],
"extras": []
},
{
"number": 35,
"name": "晉",
"judgment": "康侯用錫馬蕃庶,晝日三接。",
"lines": [
{
"label": "初六",
"text": "晉如,摧如,貞吉。罔孚,裕無咎。"
},
{
"label": "六二",
"text": "晉如,愁如,貞吉。受茲介福,于其王母。"
},
{
"label": "六三",
"text": "眾允,悔亡。"
},
{
"label": "九四",
"text": "晉如鼫鼠,貞厲。"
},
{
"label": "六五",
"text": "悔亡,失得勿恤,往吉無不利。"
},
{
"label": "上九",
"text": "晉其角,維用伐邑,厲吉無咎,貞吝。"
}
],
"extras": []
},
{
"number": 36,
"name": "明夷",
"judgment": "利艱貞。",
"lines": [
{
"label": "初九",
"text": "明夷于飛,垂其翼。君子于行,三日不食,有攸往,主人有言。"
},
{
"label": "六二",
"text": "明夷,夷于左股,用拯馬壯,吉。"
},
{
"label": "九三",
"text": "明夷于南狩,得其大首,不可疾貞。"
},
{
"label": "六四",
"text": "入于左腹,獲明夷之心,于出門庭。"
},
{
"label": "六五",
"text": "箕子之明夷,利貞。"
},
{
"label": "上六",
"text": "不明晦,初登于天,後入于地。"
}
],
"extras": []
},
{
"number": 37,
"name": "家人",
"judgment": "利女貞。",
"lines": [
{
"label": "初九",
"text": "閑有家,悔亡。"
},
{
"label": "六二",
"text": "無攸遂,在中饋,貞吉。"
},
{
"label": "九三",
"text": "家人嗃嗃,悔厲吉;婦子嘻嘻,終吝。"
},
{
"label": "六四",
"text": "富家,大吉。"
},
{
"label": "九五",
"text": "王假有家,勿恤吉。"
},
{
"label": "上九",
"text": "有孚威如,終吉。"
}
],
"extras": []
},
{
"number": 38,
"name": "睽",
"judgment": "小事吉。",
"lines": [
{
"label": "初九",
"text": "悔亡,喪馬勿逐,自復;見惡人無咎。"
},
{
"label": "九二",
"text": "遇主于巷,無咎。"
},
{
"label": "六三",
"text": "見輿曳,其牛掣,其人天且劓,無初有終。"
},
{
"label": "九四",
"text": "睽孤,遇元夫,交孚,厲無咎。"
},
{
"label": "六五",
"text": "悔亡,厥宗噬膚,往何咎。"
},
{
"label": "上九",
"text": "睽孤,見豕負塗,載鬼一車,先張之弧,後說之弧,匪寇婚媾,往遇雨則吉。"
}
],
"extras": []
},
{
"number": 39,
"name": "蹇",
"judgment": "利西南,不利東北;利見大人,貞吉。",
"lines": [
{
"label": "初六",
"text": "往蹇,來譽。"
},
{
"label": "六二",
"text": "王臣蹇蹇,匪躬之故。"
},
{
"label": "九三",
"text": "往蹇來反。"
},
{
"label": "六四",
"text": "往蹇來連。"
},
{
"label": "九五",
"text": "大蹇朋來。"
},
{
"label": "上六",
"text": "往蹇來碩,吉;利見大人。"
}
],
"extras": []
},
{
"number": 40,
"name": "解",
"judgment": "利西南,無所往,其來復吉。有攸往,夙吉。",
"lines": [
{
"label": "初六",
"text": "無咎。"
},
{
"label": "九二",
"text": "田獲三狐,得黃矢,貞吉。"
},
{
"label": "六三",
"text": "負且乘,致寇至,貞吝。"
},
{
"label": "九四",
"text": "解而拇,朋至斯孚。"
},
{
"label": "六五",
"text": "君子維有解,吉;有孚于小人。"
},
{
"label": "上六",
"text": "公用射隼,于高墉之上,獲之,無不利。"
}
],
"extras": []
},
{
"number": 41,
"name": "損",
"judgment": "有孚,元吉,無咎,可貞,利有攸往?曷之用,二簋可用享。",
"lines": [
{
"label": "初九",
"text": "已事遄往,無咎,酌損之。"
},
{
"label": "九二",
"text": "利貞,征凶,弗損益之。"
},
{
"label": "六三",
"text": "三人行,則損一人;一人行,則得其友。"
},
{
"label": "六四",
"text": "損其疾,使遄有喜,無咎。"
},
{
"label": "六五",
"text": "或益之,十朋之龜弗克違,元吉。"
},
{
"label": "上九",
"text": "弗損益之,無咎,貞吉,利有攸往,得臣無家。"
}
],
"extras": []
},
{
"number": 42,
"name": "益",
"judgment": "利有攸往,利涉大川。",
"lines": [
{
"label": "初九",
"text": "利用為大作,元吉,無咎。"
},
{
"label": "六二",
"text": "或益之,十朋之龜弗克違,永貞吉。王用享于帝,吉。"
},
{
"label": "六三",
"text": "益之用凶事,無咎。有孚中行,告公用圭。"
},
{
"label": "六四",
"text": "中行,告公從。利用為依遷國。"
},
{
"label": "九五",
"text": "有孚惠心,勿問元吉。有孚惠我德。"
},
{
"label": "上九",
"text": "莫益之,或擊之,立心勿恆,凶。"
}
],
"extras": []
},
{
"number": 43,
"name": "夬",
"judgment": "揚于王庭,孚號,有厲,告自邑,不利即戎,利有攸往。",
"lines": [
{
"label": "初九",
"text": "壯于前趾,往不勝為吝。"
},
{
"label": "九二",
"text": "惕號,莫夜有戎,勿恤。"
},
{
"label": "九三",
"text": "壯于頄,有凶。君子夬夬,獨行遇雨,若濡有慍,無咎。"
},
{
"label": "九四",
"text": "臀無膚,其行次且。牽羊悔亡,聞言不信。"
},
{
"label": "九五",
"text": "莧陸夬夬,中行無咎。"
},
{
"label": "上六",
"text": "無號,終有凶。"
}
],
"extras": []
},
{
"number": 44,
"name": "姤",
"judgment": "女壯,勿用取女。",
"lines": [
{
"label": "初六",
"text": "繫于金柅,貞吉,有攸往,見凶,羸豕孚蹢躅。"
},
{
"label": "九二",
"text": "包有魚,無咎,不利賓。"
},
{
"label": "九三",
"text": "臀無膚,其行次且,厲,無大咎。"
},
{
"label": "九四",
"text": "包無魚,起凶。"
},
{
"label": "九五",
"text": "以杞包瓜,含章,有隕自天。"
},
{
"label": "上九",
"text": "姤其角,吝,無咎。"
}
],
"extras": []
},
{
"number": 45,
"name": "萃",
"judgment": "亨。王假有廟,利見大人,亨,利貞。用大牲吉,利有攸往。",
"lines": [
{
"label": "初六",
"text": "有孚不終,乃亂乃萃,若號一握為笑,勿恤,往無咎。"
},
{
"label": "六二",
"text": "引吉,無咎,孚乃利用禴。"
},
{
"label": "六三",
"text": "萃如,嗟如,無攸利,往無咎,小吝。"
},
{
"label": "九四",
"text": "大吉,無咎。"
},
{
"label": "九五",
"text": "萃有位,無咎。匪孚,元永貞,悔亡。"
},
{
"label": "上六",
"text": "齎咨涕洟,無咎。"
}
],
"extras": []
},
{
"number": 46,
"name": "升",
"judgment": "元亨,用見大人,勿恤,南征吉。",
"lines": [
{
"label": "初六",
"text": "允升,大吉。"
},
{
"label": "九二",
"text": "孚乃利用禴,無咎。"
},
{
"label": "九三",
"text": "升虛邑。"
},
{
"label": "六四",
"text": "王用亨于岐山,吉無咎。"
},
{
"label": "六五",
"text": "貞吉,升階。"
},
{
"label": "上六",
"text": "冥升,利于不息之貞。"
}
],
"extras": []
},
{
"number": 47,
"name": "困",
"judgment": "亨,貞,大人吉,無咎,有言不信。",
"lines": [
{
"label": "初六",
"text": "臀困于株木,入于幽谷,三歲不覿。"
},
{
"label": "九二",
"text": "困于酒食,朱紱方來,利用亨祀,征凶,無咎。"
},
{
"label": "六三",
"text": "困于石,據于蒺藜,入于其宮,不見其妻,凶。"
},
{
"label": "九四",
"text": "來徐徐,困于金車,吝,有終。"
},
{
"label": "九五",
"text": "劓刖,困于赤紱,乃徐有說,利用祭祀。"
},
{
"label": "上六",
"text": "困于葛藟,于臲卼,曰動悔。有悔,征吉。"
}
],
"extras": []
},
{
"number": 48,
"name": "井",
"judgment": "改邑不改井,無喪無得,往來井井。汔至,亦未繘井,羸其瓶,凶。",
"lines": [
{
"label": "初六",
"text": "井泥不食,舊井無禽。"
},
{
"label": "九二",
"text": "井谷射鮒,甕敝漏。"
},
{
"label": "九三",
"text": "井渫不食,為我民惻,可用汲,王明,並受其福。"
},
{
"label": "六四",
"text": "井甃,無咎。"
},
{
"label": "九五",
"text": "井冽,寒泉食。"
},
{
"label": "上六",
"text": "井收勿幕,有孚無吉。"
}
],
"extras": []
},
{
"number": 49,
"name": "革",
"judgment": "己日乃孚,元亨利貞,悔亡。",
"lines": [
{
"label": "初九",
"text": "鞏用黃牛之革。"
},
{
"label": "六二",
"text": "己日乃革之,征吉,無咎。"
},
{
"label": "九三",
"text": "征凶,貞厲,革言三就,有孚。"
},
{
"label": "九四",
"text": "悔亡,有孚改命,吉。"
},
{
"label": "九五",
"text": "大人虎變,未占有孚。"
},
{
"label": "上六",
"text": "君子豹變,小人革面,征凶,居貞吉。"
}
],
"extras": []
},
{
"number": 50,
"name": "鼎",
"judgment": "元吉,亨。",
"lines": [
{
"label": "初六",
"text": "鼎顛趾,利出否,得妾以其子,無咎。"
},
{
"label": "九二",
"text": "鼎有實,我仇有疾,不我能即,吉。"
},
{
"label": "九三",
"text": "鼎耳革,其行塞,雉膏不食,方雨虧悔,終吉。"
},
{
"label": "九四",
"text": "鼎折足,覆公餗,其形渥,凶。"
},
{
"label": "六五",
"text": "鼎黃耳金鉉,利貞。"
},
{
"label": "上九",
"text": "鼎玉鉉,大吉,無不利。"
}
],
"extras": []
},
{
"number": 51,
"name": "震",
"judgment": "亨。震來虩虩,笑言啞啞。震驚百里,不喪匕鬯。",
"lines": [
{
"label": "初九",
"text": "震來虩虩,後笑言啞啞,吉。"
},
{
"label": "六二",
"text": "震來厲,億喪貝,躋于九陵,勿逐,七日得。"
},
{
"label": "六三",
"text": "震蘇蘇,震行無眚。"
},
{
"label": "九四",
"text": "震遂泥。"
},
{
"label": "六五",
"text": "震往來厲,億無喪,有事。"
},
{
"label": "上六",
"text": "震索索,視矍矍,征凶。震不于其躬,于其鄰,無咎。婚媾有言。"
}
],
"extras": []
},
{
"number": 52,
"name": "艮",
"judgment": "艮其背,不獲其身,行其庭,不見其人,無咎。",
"lines": [
{
"label": "初六",
"text": "艮其趾,無咎,利永貞。"
},
{
"label": "六二",
"text": "艮其腓,不拯其隨,其心不快。"
},
{
"label": "九三",
"text": "艮其限,列其夤,厲薰心。"
},
{
"label": "六四",
"text": "艮其身,無咎。"
},
{
"label": "六五",
"text": "艮其輔,言有序,悔亡。"
},
{
"label": "上九",
"text": "敦艮,吉。"
}
],
"extras": []
},
{
"number": 53,
"name": "漸",
"judgment": "女歸吉,利貞。",
"lines": [
{
"label": "初六",
"text": "鴻漸于干,小子厲,有言,無咎。"
},
{
"label": "六二",
"text": "鴻漸于磐,飲食衎衎,吉。"
},
{
"label": "九三",
"text": "鴻漸于陸,夫征不復,婦孕不育,凶;利御寇。"
},
{
"label": "六四",
"text": "鴻漸于木,或得其桷,無咎。"
},
{
"label": "九五",
"text": "鴻漸于陵,婦三歲不孕,終莫之勝,吉。"
},
{
"label": "上九",
"text": "鴻漸于逵,其羽可用為儀,吉。"
}
],
"extras": []
},
{
"number": 54,
"name": "歸妹",
"judgment": "征凶,無攸利。",
"lines": [
{
"label": "初九",
"text": "歸妹以娣,跛能履,征吉。"
},
{
"label": "九二",
"text": "眇能視,利幽人之貞。"
},
{
"label": "六三",
"text": "歸妹以須,反歸以娣。"
},
{
"label": "九四",
"text": "歸妹愆期,遲歸有時。"
},
{
"label": "六五",
"text": "帝乙歸妹,其君之袂,不如其娣之袂良,月幾望,吉。"
},
{
"label": "上六",
"text": "女承筐無實,士刲羊無血,無攸利。"
}
],
"extras": []
},
{
"number": 55,
"name": "豐",
"judgment": "亨,王假之,勿憂,宜日中。",
"lines": [
{
"label": "初九",
"text": "遇其配主,雖旬無咎,往有尚。"
},
{
"label": "六二",
"text": "豐其蔀,日中見斗,往得疑疾,有孚發若,吉。"
},
{
"label": "九三",
"text": "豐其沛,日中見沫,折其右肱,無咎。"
},
{
"label": "九四",
"text": "豐其蔀,日中見斗,遇其夷主,吉。"
},
{
"label": "六五",
"text": "來章,有慶譽,吉。"
},
{
"label": "上六",
"text": "豐其屋,蔀其家,窺其戶,闃其無人,三歲不覿,凶。"
}
],
"extras": []
},
{
"number": 56,
"name": "旅",
"judgment": "小亨,旅貞吉。",
"lines": [
{
"label": "初六",
"text": "旅瑣瑣,斯其所取災。"
},
{
"label": "六二",
"text": "旅即次,懷其資,得童僕貞。"
},
{
"label": "九三",
"text": "旅焚其次,喪其童僕,貞厲。"
},
{
"label": "九四",
"text": "旅于處,得其資斧,我心不快。"
},
{
"label": "六五",
"text": "射雉一矢亡,終以譽命。"
},
{
"label": "上九",
"text": "鳥焚其巢,旅人先笑後號咷。喪牛于易,凶。"
}
],
"extras": []
},
{
"number": 57,
"name": "巽",
"judgment": "小亨,利攸往,利見大人。",
"lines": [
{
"label": "初六",
"text": "進退,利武人之貞。"
},
{
"label": "九二",
"text": "巽在床下,用史巫紛若,吉無咎。"
},
{
"label": "九三",
"text": "頻巽,吝。"
},
{
"label": "六四",
"text": "悔亡,田獲三品。"
},
{
"label": "九五",
"text": "貞吉悔亡,無不利。無初有終,先庚三日,後庚三日,吉。"
},
{
"label": "上九",
"text": "巽在床下,喪其資斧,貞凶。"
}
],
"extras": []
},
{
"number": 58,
"name": "兌",
"judgment": "亨,利貞。",
"lines": [
{
"label": "初九",
"text": "和兌,吉。"
},
{
"label": "九二",
"text": "孚兌,吉,悔亡。"
},
{
"label": "六三",
"text": "來兌,凶。"
},
{
"label": "九四",
"text": "商兌,未寧,介疾有喜。"
},
{
"label": "九五",
"text": "孚于剝,有厲。"
},
{
"label": "上六",
"text": "引兌。"
}
],
"extras": []
},
{
"number": 59,
"name": "渙",
"judgment": "亨。王假有廟,利涉大川,利貞。",
"lines": [
{
"label": "初六",
"text": "用拯馬壯,吉。"
},
{
"label": "九二",
"text": "渙奔其機,悔亡。"
},
{
"label": "六三",
"text": "渙其躬,無悔。"
},
{
"label": "六四",
"text": "渙其群,元吉。渙有丘,匪夷所思。"
},
{
"label": "九五",
"text": "渙汗其大號,渙王居,無咎。"
},
{
"label": "上九",
"text": "渙其血,去逖出,無咎。"
}
],
"extras": []
},
{
"number": 60,
"name": "節",
"judgment": "亨。苦節不可貞。",
"lines": [
{
"label": "初九",
"text": "不出戶庭,無咎。"
},
{
"label": "九二",
"text": "不出門庭,凶。"
},
{
"label": "六三",
"text": "不節若,則嗟若,無咎。"
},
{
"label": "六四",
"text": "安節,亨。"
},
{
"label": "九五",
"text": "甘節,吉;往有尚。"
},
{
"label": "上六",
"text": "苦節,貞凶,悔亡。"
}
],
"extras": []
},
{
"number": 61,
"name": "中孚",
"judgment": "豚魚,吉,利涉大川,利貞。",
"lines": [
{
"label": "初九",
"text": "虞吉,有他不燕。"
},
{
"label": "九二",
"text": "鳴鶴在陰,其子和之,我有好爵,吾與爾靡之。"
},
{
"label": "六三",
"text": "得敵,或鼓或罷,或泣或歌。"
},
{
"label": "六四",
"text": "月幾望,馬匹亡,無咎。"
},
{
"label": "九五",
"text": "有孚攣如,無咎。"
},
{
"label": "上九",
"text": "翰音登于天,貞凶。"
}
],
"extras": []
},
{
"number": 62,
"name": "小過",
"judgment": "亨,利貞,可小事,不可大事。飛鳥遺之音,不宜上宜下,大吉。",
"lines": [
{
"label": "初六",
"text": "飛鳥以凶。"
},
{
"label": "六二",
"text": "過其祖,遇其妣;不及其君,遇其臣;無咎。"
},
{
"label": "九三",
"text": "弗過防之,從或戕之,凶。"
},
{
"label": "九四",
"text": "無咎,弗過遇之。往厲必戒,勿用永貞。"
},
{
"label": "六五",
"text": "密雲不雨,自我西郊,公弋取彼在穴。"
},
{
"label": "上六",
"text": "弗遇過之,飛鳥離之,凶,是謂災眚。"
}
],
"extras": []
},
{
"number": 63,
"name": "既濟",
"judgment": "亨,小利貞,初吉終亂。",
"lines": [
{
"label": "初九",
"text": "曳其輪,濡其尾,無咎。"
},
{
"label": "六二",
"text": "婦喪其茀,勿逐,七日得。"
},
{
"label": "九三",
"text": "高宗伐鬼方,三年克之,小人勿用。"
},
{
"label": "六四",
"text": "繻有衣袽,終日戒。"
},
{
"label": "九五",
"text": "東鄰殺牛,不如西鄰之禴祭,實受其福。"
},
{
"label": "上六",
"text": "濡其首,厲。"
}
],
"extras": []
},
{
"number": 64,
"name": "未濟",
"judgment": "亨,小狐汔濟,濡其尾,無攸利。",
"lines": [
{
"label": "初六",
"text": "濡其尾,吝。"
},
{
"label": "九二",
"text": "曳其輪,貞吉。"
},
{
"label": "六三",
"text": "未濟,征凶,利涉大川。"
},
{
"label": "九四",
"text": "貞吉,悔亡,震用伐鬼方,三年有賞于大國。"
},
{
"label": "六五",
"text": "貞吉,無悔,君子之光,有孚,吉。"
},
{
"label": "上九",
"text": "有孚于飲酒,無咎,濡其首,有孚失是。"
}
],
"extras": []
}
];
if (typeof window !== "undefined") window.ZHOUYI_BENJING = ZHOUYI_BENJING;
if (typeof module !== "undefined") module.exports = ZHOUYI_BENJING;
FILE:app.js
const TRIGRAMS = {
"111": { name: "乾", symbol: "☰", nature: "天", image: "健", counsel: "主动、开创、守正,不以强势替代判断。" },
"110": { name: "兑", symbol: "☱", nature: "澤", image: "悦", counsel: "沟通、交换、悦纳,但承诺要有边界。" },
"101": { name: "离", symbol: "☲", nature: "火", image: "丽", counsel: "看见事实,辨明依附关系,不被表象带走。" },
"100": { name: "震", symbol: "☳", nature: "雷", image: "动", counsel: "启动、警醒,先稳住第一步。" },
"011": { name: "巽", symbol: "☴", nature: "風", image: "入", counsel: "渐入、顺势,用连续的小动作推进。" },
"010": { name: "坎", symbol: "☵", nature: "水", image: "险", counsel: "面对风险,先建承载与退路。" },
"001": { name: "艮", symbol: "☶", nature: "山", image: "止", counsel: "止步、界限,在停顿中重新定位。" },
"000": { name: "坤", symbol: "☷", nature: "地", image: "顺", counsel: "承接、养成,以耐心和秩序承载变化。" }
};
// Key is lower trigram | upper trigram, because lines are stored from bottom to top.
const HEXAGRAM_LOOKUP = {
"乾|乾": 1, "乾|兑": 43, "乾|离": 14, "乾|震": 34, "乾|巽": 9, "乾|坎": 5, "乾|艮": 26, "乾|坤": 11,
"兑|乾": 10, "兑|兑": 58, "兑|离": 38, "兑|震": 54, "兑|巽": 61, "兑|坎": 60, "兑|艮": 41, "兑|坤": 19,
"离|乾": 13, "离|兑": 49, "离|离": 30, "离|震": 55, "离|巽": 37, "离|坎": 63, "离|艮": 22, "离|坤": 36,
"震|乾": 25, "震|兑": 17, "震|离": 21, "震|震": 51, "震|巽": 42, "震|坎": 3, "震|艮": 27, "震|坤": 24,
"巽|乾": 44, "巽|兑": 28, "巽|离": 50, "巽|震": 32, "巽|巽": 57, "巽|坎": 48, "巽|艮": 18, "巽|坤": 46,
"坎|乾": 6, "坎|兑": 47, "坎|离": 64, "坎|震": 40, "坎|巽": 59, "坎|坎": 29, "坎|艮": 4, "坎|坤": 7,
"艮|乾": 33, "艮|兑": 31, "艮|离": 56, "艮|震": 62, "艮|巽": 53, "艮|坎": 39, "艮|艮": 52, "艮|坤": 15,
"坤|乾": 12, "坤|兑": 45, "坤|离": 35, "坤|震": 16, "坤|巽": 20, "坤|坎": 8, "坤|艮": 23, "坤|坤": 2
};
const LINE_POSITIONS = ["初爻", "二爻", "三爻", "四爻", "五爻", "上爻"];
const JOURNAL_KEY = "zhouyi-benjing-journal-v1";
const LINE_GUIDANCE = [
"事在初起,宜先试探根基,不急于定局。",
"渐入关系或结构,重在取得中位的支撑。",
"内外交界,最容易躁进,宜复核风险。",
"开始外显,适合调整表达、位置与策略。",
"居于核心,重在正当性、责任和判断。",
"一事将极,宜收束、转化,不恋旧势。"
];
const QUESTION_GUIDANCE = {
relationship: "感情与关系之问,重点看互信、位置是否相称,以及是否有可持续的回应。",
work: "事业与项目之问,重点看时机、角色、资源和下一步是否可验证。",
money: "财务之问,重点先分清事实、假设与欲望;重大投入必须保留缓冲和退出条件。",
wellbeing: "身心之问,卦象只能提示节奏和边界;涉及疾病、持续痛苦或风险时,应优先求助专业人士。",
timing: "时机之问,重点看动爻多少、变卦方向,以及当下是否宜动、宜守或宜缓。",
general: "一般问题,先把卦象当成镜子:找出你真正能负责的一步,再观察现实反馈。"
};
const state = {
method: "coin",
currentReading: null,
catalogGrade: "all"
};
const els = {
question: document.querySelector("#question"),
castButton: document.querySelector("#castButton"),
modeButtons: document.querySelectorAll(".mode-button"),
resultLayout: document.querySelector("#resultLayout"),
detailGrid: document.querySelector("#detailGrid"),
primaryHexagram: document.querySelector("#primaryHexagram"),
changedHexagram: document.querySelector("#changedHexagram"),
primaryTitle: document.querySelector("#primaryTitle"),
changedTitle: document.querySelector("#changedTitle"),
primaryTrigrams: document.querySelector("#primaryTrigrams"),
changedTrigrams: document.querySelector("#changedTrigrams"),
changedBlock: document.querySelector("#changedBlock"),
changeArrow: document.querySelector("#changeArrow"),
readingText: document.querySelector("#readingText"),
lineList: document.querySelector("#lineList"),
symbolList: document.querySelector("#symbolList"),
journalList: document.querySelector("#journalList"),
copyButton: document.querySelector("#copyButton"),
clearJournalButton: document.querySelector("#clearJournalButton"),
catalogSearch: document.querySelector("#catalogSearch"),
catalogFilters: document.querySelector("#catalogFilters"),
catalogGrid: document.querySelector("#catalogGrid"),
hexagramSearch: document.querySelector("#hexagramSearch"),
hexagramLibrary: document.querySelector("#hexagramLibrary")
};
function tossLine(method) {
if (method === "yarrow") {
return createLine(weightedPick([
{ value: 6, weight: 1 },
{ value: 7, weight: 5 },
{ value: 8, weight: 7 },
{ value: 9, weight: 3 }
]));
}
const coins = Array.from({ length: 3 }, () => (Math.random() < 0.5 ? 2 : 3));
return createLine(coins.reduce((sum, coin) => sum + coin, 0), coins);
}
function weightedPick(items) {
const total = items.reduce((sum, item) => sum + item.weight, 0);
let cursor = Math.random() * total;
for (const item of items) {
cursor -= item.weight;
if (cursor < 0) return item.value;
}
return items[items.length - 1].value;
}
function createLine(value, coins = []) {
return {
value,
coins,
yang: value === 7 || value === 9,
moving: value === 6 || value === 9,
label: value === 6 ? "老阴" : value === 7 ? "少阳" : value === 8 ? "少阴" : "老阳"
};
}
function castHexagram() {
const lines = Array.from({ length: 6 }, () => tossLine(state.method));
const changedLines = lines.map((line) => ({
...line,
yang: line.moving ? !line.yang : line.yang,
moving: false
}));
return enrichReading({
id: createId(),
createdAt: new Date().toISOString(),
question: els.question.value.trim(),
method: state.method,
lines,
changedLines
});
}
function enrichReading(reading) {
const primary = resolveHexagram(reading.lines);
const changed = resolveHexagram(reading.changedLines);
const moving = reading.lines
.map((line, index) => ({ line, index }))
.filter(({ line }) => line.moving);
return {
...reading,
primary,
changed,
moving,
decision: decideTextSource(primary, changed, moving)
};
}
function createId() {
if (window.crypto?.randomUUID) return window.crypto.randomUUID();
return `reading-Date.now()-Math.random().toString(16).slice(2)`;
}
function resolveHexagram(lines) {
const lowerBits = lines.slice(0, 3).map((line) => (line.yang ? "1" : "0")).join("");
const upperBits = lines.slice(3, 6).map((line) => (line.yang ? "1" : "0")).join("");
const lower = TRIGRAMS[lowerBits];
const upper = TRIGRAMS[upperBits];
const number = HEXAGRAM_LOOKUP[`lower.name|upper.name`];
const benjing = ZHOUYI_BENJING[number - 1];
return {
number,
name: benjing.name,
fullName: fullHexagramName(benjing.name, upper, lower),
judgment: benjing.judgment,
lines: benjing.lines,
extras: benjing.extras,
lower,
upper
};
}
function fullHexagramName(name, upper, lower) {
if (upper.name === lower.name && sameTrigramName(name, upper.name)) return `name為upper.nature`;
return `upper.naturelower.naturename`;
}
function sameTrigramName(sourceName, trigramName) {
const aliases = { 兑: "兌", 离: "離" };
return sourceName === trigramName || sourceName === aliases[trigramName];
}
function decideTextSource(primary, changed, moving) {
const count = moving.length;
if (count === 0) {
return {
rule: "六爻不变:以本卦卦辞为主。",
focus: "本卦卦辞",
entries: [{ title: `primary.name卦辞`, text: primary.judgment, priority: true }]
};
}
if (count === 1) {
const item = moving[0];
return {
rule: "一爻变:以该动爻爻辞为主。",
focus: primary.lines[item.index].label,
entries: [lineEntry(primary, item.index, true)]
};
}
if (count === 2) {
const indexes = moving.map(({ index }) => index).sort((a, b) => a - b);
const upperIndex = indexes[indexes.length - 1];
return {
rule: "二爻变:取两条动爻爻辞,以上爻为主。",
focus: primary.lines[upperIndex].label,
entries: indexes.map((index) => lineEntry(primary, index, index === upperIndex))
};
}
if (count === 3) {
return {
rule: "三爻变:以本卦卦辞与变卦卦辞合看。",
focus: "本卦与变卦卦辞",
entries: [
{ title: `primary.name卦辞`, text: primary.judgment, priority: true },
{ title: `changed.name卦辞`, text: changed.judgment, priority: false }
]
};
}
if (count === 4) {
const staticIndexes = [0, 1, 2, 3, 4, 5].filter((index) => !moving.some((item) => item.index === index));
const lowerIndex = staticIndexes[0];
return {
rule: "四爻变:取两条静爻爻辞,以下爻为主。",
focus: primary.lines[lowerIndex].label,
entries: staticIndexes.map((index) => lineEntry(primary, index, index === lowerIndex))
};
}
if (count === 5) {
const staticIndex = [0, 1, 2, 3, 4, 5].find((index) => !moving.some((item) => item.index === index));
return {
rule: "五爻变:取变卦中唯一静爻所对应的爻辞。",
focus: changed.lines[staticIndex].label,
entries: [lineEntry(changed, staticIndex, true)]
};
}
const special = primary.number === 1 ? primary.extras.find((item) => item.label === "用九")
: primary.number === 2 ? primary.extras.find((item) => item.label === "用六")
: null;
if (special) {
return {
rule: "六爻皆变:乾用用九,坤用用六。",
focus: special.label,
entries: [{ title: special.label, text: special.text, priority: true }]
};
}
return {
rule: "六爻皆变:乾坤之外,以变卦卦辞为主。",
focus: `changed.name卦辞`,
entries: [{ title: `changed.name卦辞`, text: changed.judgment, priority: true }]
};
}
function lineEntry(hexagram, index, priority) {
const source = hexagram.lines[index];
return {
title: `hexagram.namesource.label`,
text: source.text,
priority
};
}
function renderReading(reading) {
state.currentReading = reading;
els.resultLayout.hidden = false;
els.detailGrid.hidden = false;
renderHexagram(els.primaryHexagram, reading.lines);
renderHexagram(els.changedHexagram, reading.changedLines);
const hasMoving = reading.moving.length > 0;
els.changedBlock.style.opacity = hasMoving ? "1" : "0.45";
els.changeArrow.textContent = hasMoving ? "→" : "•";
els.primaryTitle.textContent = `reading.primary.number. reading.primary.fullName`;
els.changedTitle.textContent = hasMoving ? `reading.changed.number. reading.changed.fullName` : "无变卦";
els.primaryTrigrams.textContent = trigramText(reading.primary);
els.changedTrigrams.textContent = hasMoving ? trigramText(reading.changed) : "六爻皆静,重在本卦卦辞。";
els.readingText.innerHTML = buildReadingSections(reading);
els.lineList.innerHTML = buildLineList(reading);
els.symbolList.innerHTML = buildSymbolList(reading);
saveJournal(reading);
renderJournal();
els.resultLayout.scrollIntoView({ behavior: "smooth", block: "start" });
}
function renderHexagram(container, lines) {
container.innerHTML = "";
[...lines].reverse().forEach((line) => {
const div = document.createElement("div");
div.className = `yao "yin" ""`;
div.setAttribute("aria-label", `line.label""`);
if (line.moving) {
const dot = document.createElement("span");
dot.className = "move-dot";
div.appendChild(dot);
}
container.appendChild(div);
});
}
function trigramText(hexagram) {
return `上hexagram.upper.namehexagram.upper.symbolhexagram.upper.nature · 下hexagram.lower.namehexagram.lower.symbolhexagram.lower.nature`;
}
function buildReadingSections(reading) {
const questionType = classifyQuestion(reading.question);
const question = reading.question || "未写下具体问题";
const entries = reading.decision.entries
.map((entry) => `<blockquote class=""""><strong>escapeHtml(entry.title)</strong>escapeHtml(entry.text)</blockquote>`)
.join("");
return `
<section>
<h3>所问</h3>
<p>escapeHtml(question)</p>
</section>
<section>
<h3>卦辞</h3>
<blockquote><strong>escapeHtml(reading.primary.name)卦辞</strong>escapeHtml(reading.primary.judgment)</blockquote>
reading.moving.length ? `<p>变卦为「${escapeHtml(reading.changed.fullName)」,其卦辞为:escapeHtml(reading.changed.judgment)</p>` : ""}
</section>
<section>
<h3>取辞</h3>
<p><span class="rule-badge">reading.moving.length 动爻</span>escapeHtml(reading.decision.rule) 本次重点:escapeHtml(reading.decision.focus)。</p>
<div class="source-stack">entries</div>
</section>
<section>
<h3>解读</h3>
<p>buildInterpretation(reading, questionType)</p>
</section>
<section>
<h3>体系路由</h3>
<p>buildRouteNote(reading, questionType)</p>
</section>
<section>
<h3>行动</h3>
<p>buildActionAdvice(reading, questionType)</p>
</section>
<section>
<h3>追问</h3>
<p>buildReflectionQuestion(reading, questionType)</p>
</section>
`;
}
function buildRouteNote(reading, questionType) {
const enabled = DIVINATION_SYSTEMS.find((item) => item.id === "zhouyi-benjing");
const related = recommendSystems(questionType)
.map((id) => DIVINATION_SYSTEMS.find((item) => item.id === id))
.filter(Boolean);
const names = related.map((item) => `item.name(item.status,item.grade级)`).join(";");
return `本次采用 enabled.name(enabled.grade级,enabled.status)作为主体系,因为它能直接引用本经并按动爻取辞。可参考的旁支体系:names || "暂无"。旁支只作百科线索,不参与本次断语。`;
}
function recommendSystems(questionType) {
const map = {
relationship: ["liuyao", "meihua", "ziwei", "tarot"],
work: ["meihua", "liuyao", "qimen", "bazi"],
money: ["liuyao", "qimen", "bazi"],
wellbeing: ["meihua", "fengshui", "bazi"],
timing: ["qimen", "liuyao", "meihua"],
general: ["meihua", "liuyao", "routing"]
};
return map[questionType] || map.general;
}
function buildInterpretation(reading, questionType) {
const movement = reading.moving.length === 0
? "此卦无动爻,说明问题的关键不在立刻改变,而在看清本卦所呈现的结构。"
: reading.moving.length >= 4
? "动爻很多,表示局面已经接近整体翻转,判断时要少抓单点,多看变卦给出的方向。"
: "有动爻,说明现状中已经出现转折点;爻位提示变化发生的层次。";
const lineLayer = reading.moving.length
? reading.moving.map(({ index }) => `LINE_POSITIONS[index]:LINE_GUIDANCE[index]`).join(" ")
: "六爻皆静,以本卦卦辞为主,不宜把问题解释成马上会变。";
return `movementlineLayerQUESTION_GUIDANCE[questionType] 本系统只以《周易》本经卦辞、爻辞为底,不把结果说成确定命令。`;
}
function buildActionAdvice(reading, questionType) {
const count = reading.moving.length;
const typeAdvice = {
relationship: "先校正关系中的位置与边界,再谈推进;如果对方回应不稳定,不要用催促替代确认。",
work: "把下一步拆成可验证的小行动,先确认角色、资源、时间表,再扩大承诺。",
money: "不要只看收益叙事,先列出最大损失、退出条件和等待成本。",
wellbeing: "先恢复秩序和支持系统;若涉及疾病或持续痛苦,请优先找专业帮助。",
timing: "若动爻少,先抓关键动作;若动爻多,缩短承诺周期,等待新局面落定。",
general: "把卦象落回现实:今天能负责的一步是什么,做完后用什么信号复盘。"
};
const rhythm = count === 0
? "宜守中观察,少做剧烈转向。"
: count <= 2
? "宜抓住一个关键点,小步推进。"
: count === 3
? "宜同时看现状与去向,先做过渡安排。"
: "宜降低赌注,给变化留出缓冲。";
return `rhythmtypeAdvice[questionType]`;
}
function buildReflectionQuestion(reading, questionType) {
const base = {
relationship: "这段关系里,我真正能负责的是表达、边界,还是等待?",
work: "如果只推进一步,哪一步最能验证这件事值得继续?",
money: "我现在看到的是价值、价格,还是被波动放大的欲望与恐惧?",
wellbeing: "我最需要先恢复的是体力、秩序、支持,还是边界?",
timing: "我是在等合适时机,还是在用等待回避行动?",
general: "这件事里,我真正能负责的部分是什么?"
};
return `base[questionType]以「reading.primary.fullName」为镜,再看「reading.changed.fullName」是否指出下一阶段。`;
}
function buildLineList(reading) {
return reading.lines
.map((line, index) => {
const source = reading.primary.lines[index];
const selected = reading.decision.entries.some((entry) => entry.title === `reading.primary.namesource.label`);
const coinText = line.coins.length ? `三钱:line.coins.join(" + ") = line.value` : `蓍草概率数:line.value`;
return `
<div class="line-item "" """>
<strong>source.label · line.label""</strong>
<p>coinText</p>
<p class="source-text">escapeHtml(source.text)</p>
</div>
`;
})
.reverse()
.join("");
}
function buildSymbolList(reading) {
const items = [
{
title: "本经底座",
text: "本页数据由 sources/zhouyi/zhouyi_benjing.txt 生成,仅含六十四卦卦辞、爻辞、用九、用六。"
},
{
title: `上卦 reading.primary.upper.namereading.primary.upper.symbol`,
text: `reading.primary.upper.nature象为reading.primary.upper.image:reading.primary.upper.counsel`
},
{
title: `下卦 reading.primary.lower.namereading.primary.lower.symbol`,
text: `reading.primary.lower.nature象为reading.primary.lower.image:reading.primary.lower.counsel`
},
{
title: "取辞规则",
text: reading.decision.rule
}
];
if (reading.moving.length) {
items.push({
title: `变化 reading.changed.fullName`,
text: `由 reading.primary.fullName 变为 reading.changed.fullName,解释时先按动爻数量取辞,再参考变卦方向。`
});
}
return items
.map(
(item) => `
<div class="symbol-item">
<strong>escapeHtml(item.title)</strong>
<p>escapeHtml(item.text)</p>
</div>
`
)
.join("");
}
function renderCatalog() {
if (!els.catalogGrid) return;
const query = normalize(els.catalogSearch?.value || "");
const systems = DIVINATION_SYSTEMS.filter((item) => {
const gradeOk = state.catalogGrade === "all" || item.grade === state.catalogGrade;
const haystack = normalize([
item.name,
item.family,
item.grade,
item.status,
item.basis,
item.capability,
item.guardrail,
item.bestFor.join(" "),
item.inputs.join(" ")
].join(" "));
return gradeOk && (!query || haystack.includes(query));
});
els.catalogGrid.innerHTML = systems.map(renderSystemCard).join("") ||
`<div class="system-card"><p>没有匹配的体系。</p></div>`;
}
function renderSystemCard(item) {
return `
<article class="system-card">
<header>
<div>
<h3>escapeHtml(item.name)</h3>
<p>escapeHtml(item.family) · escapeHtml(item.basis)</p>
</div>
<span class="grade-badge grade-item.grade.toLowerCase()">item.grade</span>
</header>
<span class="status-badge">escapeHtml(item.status)</span>
<p>escapeHtml(item.capability)</p>
<div class="tag-list">item.bestFor.slice(0, 4).map((tag) => `<span>${escapeHtml(tag)</span>`).join("")}</div>
<p><strong>资料:</strong>escapeHtml(item.inputs.join("、"))</p>
<p><strong>边界:</strong>escapeHtml(item.guardrail)</p>
</article>
`;
}
function renderHexagramLibrary() {
if (!els.hexagramLibrary) return;
const query = normalize(els.hexagramSearch?.value || "");
const items = ZHOUYI_BENJING.filter((hex) => {
const haystack = normalize([
hex.number,
hex.name,
hex.judgment,
hex.lines.map((line) => `line.labelline.text`).join(" "),
hex.extras.map((line) => `line.labelline.text`).join(" ")
].join(" "));
return !query || haystack.includes(query);
});
els.hexagramLibrary.innerHTML = items.map(renderHexCard).join("") ||
`<div class="hex-card"><p>没有匹配的卦。</p></div>`;
}
function renderHexCard(hex) {
const firstMovingLine = hex.lines.find((line) => /吉|凶|悔|咎|厲|利/.test(line.text)) || hex.lines[0];
return `
<article class="hex-card">
<header>
<h3>hex.number. escapeHtml(hex.name)</h3>
<span class="grade-badge grade-s">本经</span>
</header>
<p class="source-line">escapeHtml(hex.name):escapeHtml(hex.judgment)</p>
<p>escapeHtml(firstMovingLine.label):escapeHtml(firstMovingLine.text)</p>
</article>
`;
}
function normalize(value) {
return String(value).toLowerCase().replace(/\s+/g, "");
}
function classifyQuestion(question) {
const q = question.toLowerCase();
if (/感情|关系|恋|婚|伴侣|喜欢|复合|分手/.test(q)) return "relationship";
if (/工作|事业|合作|项目|公司|创业|offer|职位|老板|面试|跳槽/.test(q)) return "work";
if (/钱|投资|买|卖|收入|财|价格|交易|理财|资产/.test(q)) return "money";
if (/健康|身体|病|睡眠|焦虑|压力|治疗|医院/.test(q)) return "wellbeing";
if (/何时|什么时候|时机|现在适合|是否适合|能不能|该不该/.test(q)) return "timing";
return "general";
}
function methodText(method) {
if (method === "yarrow") return "蓍草概率";
return "三枚铜钱";
}
function saveJournal(reading) {
const journal = getJournal().filter((item) => item.id !== reading.id);
journal.unshift({
id: reading.id,
createdAt: reading.createdAt,
question: reading.question,
method: reading.method,
lines: reading.lines,
changedLines: reading.changedLines
});
localStorage.setItem(JOURNAL_KEY, JSON.stringify(journal.slice(0, 8)));
}
function getJournal() {
try {
return JSON.parse(localStorage.getItem(JOURNAL_KEY) || "[]");
} catch {
return [];
}
}
function renderJournal() {
const journal = getJournal();
if (!journal.length) {
els.journalList.innerHTML = `<div class="journal-item"><p>暂无记录</p></div>`;
return;
}
els.journalList.innerHTML = journal
.map((item) => {
const reading = enrichReading(item);
const date = new Intl.DateTimeFormat("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
}).format(new Date(item.createdAt));
return `
<button class="journal-item" type="button" data-id="item.id">
<strong>reading.primary.number. escapeHtml(reading.primary.fullName)</strong>
<p>date · methodText(item.method) · escapeHtml(item.question || "未写问题")</p>
</button>
`;
})
.join("");
}
function escapeHtml(value = "") {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function plainTextReading() {
if (!state.currentReading) return "";
const container = document.createElement("div");
container.innerHTML = buildReadingSections(state.currentReading);
return container.innerText.trim();
}
function bootstrap() {
if (!Array.isArray(window.ZHOUYI_BENJING) || window.ZHOUYI_BENJING.length !== 64) {
els.readingText.innerHTML = `<section><h3>数据错误</h3><p>未能加载完整《周易》本经数据。</p></section>`;
return;
}
if (!Array.isArray(window.DIVINATION_SYSTEMS) || window.DIVINATION_SYSTEMS.length < 8) {
els.readingText.innerHTML = `<section><h3>数据错误</h3><p>未能加载完整术数百科数据。</p></section>`;
return;
}
els.modeButtons.forEach((button) => {
button.addEventListener("click", () => {
state.method = button.dataset.method;
els.modeButtons.forEach((item) => item.classList.toggle("active", item === button));
});
});
els.castButton.addEventListener("click", () => {
renderReading(castHexagram());
});
els.copyButton.addEventListener("click", async () => {
const text = plainTextReading();
if (!text) return;
try {
await navigator.clipboard.writeText(text);
els.copyButton.textContent = "✓";
} catch {
els.copyButton.textContent = "!";
}
window.setTimeout(() => {
els.copyButton.textContent = "⧉";
}, 1200);
});
els.clearJournalButton.addEventListener("click", () => {
localStorage.removeItem(JOURNAL_KEY);
renderJournal();
});
els.catalogSearch?.addEventListener("input", renderCatalog);
els.hexagramSearch?.addEventListener("input", renderHexagramLibrary);
els.catalogFilters?.addEventListener("click", (event) => {
const button = event.target.closest("[data-grade]");
if (!button) return;
state.catalogGrade = button.dataset.grade;
els.catalogFilters.querySelectorAll(".filter-button").forEach((item) => {
item.classList.toggle("active", item === button);
});
renderCatalog();
});
els.journalList.addEventListener("click", (event) => {
const button = event.target.closest("[data-id]");
if (!button) return;
const record = getJournal().find((item) => item.id === button.dataset.id);
if (record) renderReading(enrichReading(record));
});
renderJournal();
renderCatalog();
renderHexagramLibrary();
}
bootstrap();
Insta360 leads global 360-degree camera innovation with AI editing, modular design, and a strong presence from consumer to industrial markets.
--- name: insta360 summary: 从深圳创业公司到全球全景相机领导者 — Insta360 如何挑战 GoPro 的统治 read_when: - 研究运动相机和全景相机市场时 - 分析中国品牌出海成功案例时 - 对比 GoPro vs Insta360 时 - 了解 AI 影像技术时 --- # Insta360 ## 概述 从深圳创业公司到全球全景相机领导者 — Insta360 如何挑战 GoPro 的统治。 ## 历史时间线 - 2015: 刘靖康(JK Liu)在深圳创立 Insta360 - 2015: 发布首款 360 度全景相机 - 2018: 推出 ONE X,获得消费级市场突破 - 2019: 推出 ONE R(模块化设计),可切换全景和标准镜头 - 2020: GO 系列(拇指相机)重新定义便携影像 - 2021-2022: X3 和 X4 系列巩固全景相机领导地位 - 2022: 推出 Flow(手机云台),扩展品类 - 2023-2024: Ace Pro 系列进入传统运动相机市场,直接竞争 GoPro ## 商业模式 消费级运动相机+全景相机硬件销售。差异化:AI 后期处理(自动编辑、隐形自拍杆、AI 追踪)、模块化设计、手机 App 生态。同时提供行业级(企业、房产、安防)全景解决方案。 ## 护城河分析 全景相机领域的技术壁垒(拼接算法、防抖、AI 编辑);创新速度远超 GoPro;模块化设计(ONE R)差异化;AI 自动编辑降低用户门槛;性价比优势。 ## 关键数据 - **总部**: 中国深圳 - **全球全景相机市占率**: 估计 40%+ - **产品覆盖**: 消费级+行业级 - **员工**: ~3,000+ ## 有趣事实 - 创始人刘靖康出生于 1991 年,24 岁创立 Insta360,被称为'90 后硬科技创业者代表' - Insta360 的'隐形自拍杆'功能通过算法自动消除画面中的自拍杆,是全景相机领域最具辨识度的功能
Convert Markdown to beautiful presentations and slides. 一键将Markdown文档转换为精美PPT幻灯片,支持多种主题风格,适合商务汇报、教学课件、会议演讲。Markdown to PPT, presentation generator, slides ma...
---
name: Markdown to Slides
description: "Convert Markdown to beautiful presentations and slides. 一键将Markdown文档转换为精美PPT幻灯片,支持多种主题风格,适合商务汇报、教学课件、会议演讲。Markdown to PPT, presentation generator, slides maker."
tags: markdown, slides, presentation, ppt, converter, deck, 演示, 幻灯片, utility, tool
---
# Markdown to Slides 🎯
Markdown转PPT演示文稿工具。
## Features | 功能
- **Markdown导入**:支持标准Markdown语法
- **多种主题**:商务/学术/创意主题
- **导出格式**:PowerPoint兼容格式
## Usage | 使用
```
# 转换Markdown为幻灯片
md2ppt.py <input.md> [output.pptx]
```
---
*免责声明:本工具仅供学习参考,不构成任何投资或商业建议。*
FILE:scripts/md2ppt.py
#!/usr/bin/env python3
"""Markdown to PowerPoint converter"""
import sys, os, re
# Check if python-pptx is available
try:
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
HAS_PPTX = True
except ImportError:
HAS_PPTX = False
def md_to_slides(md):
"""Split markdown into slides by headings"""
lines = md.split('\n')
slides = []
current = []
for line in lines:
stripped = line.strip()
if stripped.startswith('# ') and current:
slides.append('\n'.join(current))
current = [stripped]
elif stripped.startswith('## ') and current:
slides.append('\n'.join(current))
current = [stripped]
else:
current.append(stripped)
if current:
slides.append('\n'.join(current))
return slides
def parse_content(slide_md):
"""Extract title and bullet points from slide markdown"""
lines = slide_md.split('\n')
title = ""
bullets = []
in_code = False
code_content = []
for line in lines:
stripped = line.strip()
if stripped.startswith('# '):
title = stripped[2:]
elif stripped.startswith('## '):
if not title:
title = stripped[3:]
elif stripped.startswith('- ') or stripped.startswith('* '):
bullets.append(stripped[2:])
elif stripped.startswith('```'):
in_code = not in_code
elif in_code:
code_content.append(stripped)
elif stripped and not title:
if stripped not in ['', ' ']:
bullets.append(stripped)
return title, bullets, '\n'.join(code_content) if code_content else None
def create_pptx(slides, output='output.pptx', theme='professional'):
if not HAS_PPTX:
# Fallback: create HTML presentation
html = f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Presentation</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 0; }}
.slide {{ width: 100vw; height: 100vh; display: flex; flex-direction: column; justify-content: center; padding: 60px; box-sizing: border-box; page-break-after: always; }}
h1 {{ font-size: 48px; margin-bottom: 40px; color: #1a1a2e; }}
h2 {{ font-size: 36px; margin-bottom: 30px; color: #16213e; }}
li {{ font-size: 28px; margin: 15px 0; color: #333; }}
code {{ background: #f4f4f4; padding: 3px 8px; border-radius: 4px; font-family: monospace; }}
</style></head><body>
"""
for slide in slides:
title, bullets, code = parse_content(slide)
if not title:
title = "Presentation"
html += f'<div class="slide"><h1>{title}</h1>\n'
for b in bullets:
html += f'<li>{b}</li>\n'
if code:
html += f'<pre><code>{code}</code></pre>\n'
html += '</div>\n'
html += '</body></html>'
with open(output.replace('.pptx', '.html'), 'w') as f:
f.write(html)
return output.replace('.pptx', '.html')
prs = Presentation()
prs.slide_width = Inches(13.33)
prs.slide_height = Inches(7.5)
colors = {
'professional': (26, 26, 46),
'creative': (41, 128, 185),
'minimal': (50, 50, 50),
}
bg_color = colors.get(theme, colors['professional'])
for slide_md in slides:
title, bullets, code = parse_content(slide_md)
if not title:
title = "Slide"
slide_layout = prs.slide_layouts[6] # Blank
slide = prs.slides.add_slide(slide_layout)
background = slide.shapes.add_shape(1, 0, 0, prs.slide_width, prs.slide_height)
background.fill.solid()
background.fill.fore_color.rgb = RGBColor(*bg_color)
background.line.fill.background()
txTitle = slide.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(12), Inches(1.2))
tf = txTitle.text_frame
p = tf.paragraphs[0]
p.text = title
p.font.size = Pt(44)
p.font.bold = True
p.font.color.rgb = RGBColor(255, 255, 255)
if bullets:
txBody = slide.shapes.add_textbox(Inches(0.7), Inches(1.8), Inches(11.5), Inches(5))
tf = txBody.text_frame
tf.word_wrap = True
for i, bullet in enumerate(bullets[:8]):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
p.text = f"• {bullet}"
p.font.size = Pt(24)
p.font.color.rgb = RGBColor(230, 230, 230)
p.space_before = Pt(12)
if code:
txCode = slide.shapes.add_textbox(Inches(0.7), Inches(5.5), Inches(11.5), Inches(1.5))
tf = txCode.text_frame
p = tf.paragraphs[0]
p.text = code[:200]
p.font.size = Pt(14)
p.font.name = "Courier New"
p.font.color.rgb = RGBColor(150, 255, 150)
prs.save(output)
return output
def main():
args = sys.argv[1:]
md = ""
output = "output.pptx"
theme = "professional"
i = 0
while i < len(args):
if args[i] == "--file" and i + 1 < len(args):
with open(args[i+1]) as f:
md = f.read()
i += 2
elif args[i] == "--theme" and i + 1 < len(args):
theme = args[i+1]
i += 2
elif args[i] == "--output" and i + 1 < len(args):
output = args[i+1]
i += 2
else:
md += args[i] + " "
i += 1
if not md.strip():
print("Usage: md2ppt.py [--file <file.md>] [--theme professional|creative|minimal] [--output file.pptx] <markdown>", file=sys.stderr)
sys.exit(1)
slides = md_to_slides(md)
result = create_pptx(slides, output, theme)
print(f"Created: {result}")
if not HAS_PPTX:
print("(python-pptx not installed, created HTML instead)")
if __name__ == "__main__":
main()
Prioritize which assumptions to validate first and produce focused learning goals before customer conversations — classifying risks as product risk versus ma...
---
name: question-importance-prioritizer
description: Prioritize which assumptions to validate first and produce focused learning goals before customer conversations — classifying risks as product risk versus market risk. Use this skill whenever the user has many assumptions or unknowns and needs to decide which to test first, wants to identify the 3 most important learning goals for their next conversation batch, needs to figure out what the riskiest parts of their business idea are, wants to separate must-validate assumptions from safe ones, is preparing strategic learning goals but not the specific interview questions, or suspects they are avoiding the scary questions that actually matter — even if they don't mention "prioritization" or "learning goals." Do NOT use this skill to write or rewrite the actual conversation questions (use conversation-question-designer) or to analyze notes from a completed conversation (use conversation-data-quality-analyzer).
version: 1.0.0
homepage: https://github.com/bookforge-ai/bookforge-skills/tree/main/books/the-mom-test/skills/question-importance-prioritizer
metadata: {"openclaw":{"emoji":"📚","homepage":"https://github.com/bookforge-ai/bookforge-skills"}}
status: verified
source-books:
- id: the-mom-test
title: "The Mom Test"
authors: ["Rob Fitzpatrick"]
chapters: [3]
tags: [customer-discovery, question-prioritization, learning-goals, risk-classification, pre-conversation-planning]
depends-on: []
execution:
tier: 1
mode: hybrid
inputs:
- type: document
description: "Product idea description and list of assumptions or unknowns to validate"
tools-required: [Read, Write]
tools-optional: []
mcps-required: []
environment: "Any agent environment with file read/write access."
---
# Question Importance Prioritizer
## When to Use
You need to decide what to learn before customer conversations — not just which questions pass quality rules, but which questions actually matter for your business survival. Typical situations:
- The user has many assumptions to validate and needs to prioritize which 3 to focus on next
- The user is preparing for a batch of customer conversations and needs focused learning goals
- The user has been having conversations but feels stuck because they are asking safe, comfortable questions
- The user needs to determine whether their biggest risks can even be validated through conversations (product risk vs market risk)
- The user wants to identify the "scary questions" they have been avoiding
- The user has a long list of unknowns and does not know where to start
Before starting, verify:
- Does the user have a product idea or business concept? (If not, this skill cannot help yet)
- Does the user have at least a rough sense of who their customers might be? (Different customer types need different learning goals)
**Mode: Hybrid** — The agent produces the prioritized learning goals and prepared questions. The human conducts the actual conversations.
## Context & Input Gathering
### Required Context (must have — ask if missing)
- **Product idea or business concept:** What is the user building or exploring? This is the foundation for identifying risks and learning goals.
- Check prompt for: product descriptions, startup ideas, feature concepts, problem statements
- Check environment for: `product-idea.md`, `README.md`, pitch documents
- If still missing, ask: "What product or business idea are you working on? A few sentences describing what it does and who it is for."
- **Assumptions or unknowns to validate:** What does the user believe but has not yet proven? This is the raw material for prioritization.
- Check prompt for: hypotheses, assumptions lists, "I think...", "I believe...", "I assume...", risk lists
- Check environment for: `learning-log.md`, `assumptions.md`, previous conversation notes
- If still missing, ask: "What are the key assumptions your business depends on? List everything you believe to be true but have not yet validated — about your customers, the problem, the market, pricing, distribution, anything."
### Observable Context (gather from environment)
- **Customer segment:** Who is the user targeting? Different segments need different learning goals.
- Look for: `customer-segments.md`, persona descriptions, target market references
- If unavailable: ask "Who are your target customers? Be as specific as you can."
- **Current stage:** How far along is the user? Pre-idea, pre-product, has a prototype, has paying customers?
- Look for: references to prototypes, MVPs, revenue, launch dates
- If unavailable: assume pre-product (exploring the problem space)
- **Previous conversation learnings:** What has already been validated or invalidated?
- Look for: `conversation-notes/`, `learning-log.md`
- If unavailable: assume first round of conversations
### Default Assumptions
- If no customer type specified, design learning goals generic enough for early exploration but note this limitation
- If no stage specified, assume pre-product (learning phase)
- If no prior conversations, assume all assumptions are unvalidated
### Sufficiency Threshold
```
SUFFICIENT when ALL of these are true:
- Product idea or business concept is known
- At least 3 assumptions or unknowns are identified
- Customer type is known or defaulted
PROCEED WITH DEFAULTS when:
- Product idea is known but assumptions are vague ("I'm not sure what I don't know")
- Customer type is approximate ("probably restaurant owners")
MUST ASK when:
- No product idea at all
- User provides questions but no context on the business they are building
```
## Process
### Step 1: Surface All Business Risks
**ACTION:** List every assumption the business depends on — both the ones the user stated and the ones they may have missed. Use two diagnostic questions to uncover hidden risks:
1. "If this company were to fail, why would it have happened?" — list every plausible failure reason
2. "What would have to be true for this to be a huge success?" — list every condition required
**WHY:** Most founders focus on the risks they find interesting (usually the product or technology) and ignore the ones that scare them (usually the market, pricing, or distribution). The two diagnostic questions systematically surface hidden risks that the user is unconsciously avoiding. The most important questions to ask customers are precisely the ones that feel most uncomfortable.
**IF** the user provided a list of assumptions, review it against the diagnostic questions and add any missing risks
**IF** the user did not provide assumptions, generate the risk list entirely from the diagnostic questions
**OUTPUT:** A comprehensive list of business risks, grouped loosely by area (customer/problem, market/pricing, product/technology, distribution/growth, team/operations).
### Step 2: Classify Each Risk as Product Risk or Market Risk
**ACTION:** For each risk from Step 1, classify it into one of two categories:
| Risk Type | Definition | Key Questions | Can Conversations Validate? |
|-----------|-----------|---------------|---------------------------|
| **Market risk** | Do they want it? Will they pay? Are there enough of them? | Demand, willingness to pay, market size, problem severity | Yes — customer conversations are the primary validation tool |
| **Product risk** | Can I build it? Can I grow it? Will they keep using it? | Technical feasibility, scalability, retention, network effects, critical mass | Limited — you need to build something to prove these |
**WHY:** This classification determines how much weight to give conversation-based validation for each risk. If the user's biggest risk is product-side (like building a marketplace that needs critical mass, or a video game that needs to be fun), customer conversations alone cannot validate it — the user will need to start building earlier with less certainty. Mistaking product risk for market risk leads to months of conversations that "validate" obvious things (e.g., asking farmers if they want more money, asking bar owners if they want more customers).
**Detection test for product risk masquerading as market risk:** If customer responses consistently sound like "Yes, if you can actually build that, I would pay" — the risk is in the product, not the market. The customer is restating the obvious.
**IF** the majority of risks are product-side, warn the user: "Your biggest unknowns are about whether you can build and grow this, not whether people want it. Customer conversations will give you a starting point, but you will need to start building earlier to validate the core risks. Focus conversations on understanding the problem depth and current workarounds, not on confirming demand."
**OUTPUT:** Each risk annotated with its type (market/product) and whether conversations can validate it.
### Step 3: Prioritize into the Top 3 Learning Goals
**ACTION:** From the classified risk list, select the 3 most important learning goals for the next batch of conversations. Prioritize using these criteria:
1. **Business-criticality:** Could this risk, if wrong, kill the entire business? Risks that would require a complete pivot outrank risks that would require a feature adjustment.
2. **Current uncertainty:** How much evidence does the user already have? Prioritize the murkiest unknowns — the ones where the user has the least data.
3. **Conversational reach:** Can customer conversations actually answer this? Deprioritize pure product risks that need building, not talking.
4. **Scariness:** Is this a question the user has been avoiding? If a question makes the user uncomfortable, that is a signal it is important. A question you are not terrified of is probably not important enough.
**WHY:** Without prioritization, conversations wander across too many topics and produce shallow data on everything, deep data on nothing. Three is the right number because it is small enough to focus a conversation but large enough to make each conversation worthwhile. Choose the murkiest and most important questions — they will give you the firmest footing and clearest sense of direction for the next batch.
**Scary question test:** Review the final list and verify that at least one learning goal makes the user uncomfortable. If all three feel safe and easy to ask about, the list is wrong — the user is avoiding the hard questions. Flag this explicitly: "None of these learning goals seem scary. What question are you most afraid to ask? That one probably belongs on this list."
**IF** the user has multiple customer types, create a separate list of 3 for each type — learning goals differ by audience
**IF** this is not the first batch of conversations, review previous learnings and update: drop validated goals, promote the next murkiest unknowns
**OUTPUT:** A numbered list of exactly 3 learning goals, each with:
- The learning goal stated as a concrete question to answer
- Why it matters (what changes if the answer is negative)
- The risk type (market or product)
- A scariness rating (comfortable / uncomfortable / terrifying)
### Step 4: Check for Premature Zoom
**ACTION:** Review each learning goal and assess whether it assumes something that has not yet been validated. Apply the premature zoom diagnostic:
- Does this goal zoom into a specific problem area without first confirming that area matters to the customer?
- If you ask about this topic, will the customer give you detailed answers just because you asked — regardless of whether they actually care?
- Would the customer have raised this topic on their own if you asked broad questions about their life?
**WHY:** Premature zoom is one of the most dangerous patterns in customer discovery. When you ask "What is your biggest problem with X?", you assume X matters. The person gives you an answer because you asked, not because they care. This creates data that looks like validation but is actually worthless. Even if you learn everything there is to know about a trivial problem, you still do not have a business. The fix is to start broad and only zoom in when the customer independently signals that this area is a top priority for them.
**FOR EACH** learning goal:
- **IF** the goal assumes problem importance → flag it and add a broader "does this even matter?" goal that should come first
- **IF** the goal is already about confirming importance → mark it as properly scoped
- **IF** previous conversations have already confirmed importance → mark it as safe to zoom
**"Does-this-problem-matter" diagnostic questions** (use these to validate importance before zooming in):
- "How seriously do you take [area]?"
- "Do you make money from it?"
- "Have you tried making more money from it?"
- "How much time do you spend on it each week?"
- "Do you have any major aspirations for [area]?"
- "Which tools and services do you use for it?"
- "What are you already doing to improve this?"
- "What are the 3 big things you are trying to fix or improve right now?"
**OUTPUT:** Each learning goal annotated with its zoom-level safety status and, where needed, a broader prerequisite question.
### Step 5: Produce the Prioritized Learning Goals Deliverable
**ACTION:** Compile the final output document containing the prioritized learning goals with risk classification and prepared questions for each goal.
**WHY:** The deliverable must be immediately usable before conversations. The user should be able to glance at it and know exactly what they need to learn, why each goal matters, and which questions to ask. This is the "list of 3" that they carry into every conversation with this customer type.
**Output format:**
```markdown
# Prioritized Learning Goals
## Context
- **Product/Business:** [from input]
- **Target Customer:** [from input]
- **Stage:** [from input or default]
- **Date Prepared:** [today]
- **Batch:** [first / updated after N conversations]
## Risk Overview
- **Total risks identified:** [N]
- **Market risks (conversation-validatable):** [N]
- **Product risks (need building to validate):** [N]
- **Biggest overlooked risk:** [the one the user was probably avoiding]
## Top 3 Learning Goals
### 1. [Learning Goal as Question]
- **Risk type:** Market / Product
- **Why it matters:** [what changes if the answer is negative — be specific]
- **Scariness:** Comfortable / Uncomfortable / Terrifying
- **Zoom-level check:** [Safe to zoom / Needs importance confirmation first]
- **Prepared questions:**
- [Broad opener to confirm importance]
- [Specific past-focused depth question]
- [Commitment/severity signal question]
- **What a negative answer looks like:** [concrete signal that disproves this]
- **What a positive answer looks like:** [concrete signal that validates this]
### 2. [Learning Goal as Question]
[same structure]
### 3. [Learning Goal as Question]
[same structure]
## Questions You Might Be Avoiding
- [Scary question 1 — and why it matters]
- [Scary question 2 — and why it matters]
## Premature Zoom Warnings
- [Any goals that assume unvalidated importance, with the broader question to ask first]
## Risk Classification Summary
| Risk | Type | Conversation Can Validate? | Priority |
|------|------|---------------------------|----------|
| [risk 1] | Market | Yes | In top 3 |
| [risk 2] | Product | Limited | Deferred |
| ... | ... | ... | ... |
## Next Steps
- After this conversation batch, review which goals are answered
- Drop answered goals, promote next-murkiest unknowns
- Update this document with new top 3
```
**IF** the user provided a file path or working directory, write the output to `learning-goals.md`
**ELSE** present the output directly in the conversation
## Examples
### Scenario 1: SaaS Founder with a Long Assumption List
**Trigger:** "I'm building a tool that helps restaurant owners manage their online reviews across Google, Yelp, and TripAdvisor. Here are my assumptions: (1) Restaurant owners care about online reviews, (2) Managing multiple platforms is painful, (3) They would pay $50/month, (4) They check reviews daily, (5) Negative reviews cause real revenue loss, (6) They want AI-generated review responses, (7) They struggle to get customers to leave reviews."
**Process:**
1. Surface all risks: The user listed 7 assumptions, but diagnostic questions reveal hidden ones — distribution (how will they find this tool?), competition (existing tools like Podium?), buyer (is the owner the one managing reviews or a manager?), and time (do they have bandwidth to use yet another tool?)
2. Classify risks: Assumptions 1-5, 7 are market risks (conversationally validatable). Assumption 6 is product risk (AI quality). Distribution and competition are market risks.
3. Prioritize top 3:
- Goal 1: "Do restaurant owners actually manage reviews themselves, and is it painful enough to pay to fix?" (market risk, terrifying — could invalidate the whole idea)
- Goal 2: "What tools or workarounds are they using today, and what do they spend?" (market risk, uncomfortable — might reveal strong competitors)
- Goal 3: "How do they currently respond to negative reviews, and what is the real cost of not responding?" (market risk, comfortable — validates severity)
4. Premature zoom check: Goal 3 assumes negative reviews matter enough to act on — needs importance confirmation first
**Output (abbreviated):**
```
### 1. Do restaurant owners personally manage reviews — and is it painful enough to pay $50/month?
- Risk type: Market
- Why it matters: If owners delegate review management or don't care, there is no buyer
- Scariness: Terrifying
- Zoom-level check: Safe — this IS the importance check
- Prepared questions:
- "Walk me through what you did the last time you got a negative review online."
- "How much time do you spend on review-related tasks in a typical week?"
- "What are you currently paying for any marketing or reputation tools?"
- What a negative answer looks like: "My manager handles that" or "I don't really check them"
- What a positive answer looks like: Specific stories of time spent, emotional frustration, existing workarounds
```
---
### Scenario 2: Technical Founder with Pure Product Risk
**Trigger:** "I'm building a multiplayer mobile game where players collaborate to solve environmental puzzles. I want to validate whether people would play this. My assumptions: (1) People enjoy collaborative puzzle games, (2) Environmental themes attract players, (3) Mobile is the right platform, (4) Players will invite friends to join."
**Process:**
1. Surface risks: Diagnostic questions reveal the elephant — nearly all risk is product-side (Is it fun? Can it retain players? Can it achieve network effects for multiplayer?)
2. Classify risks: All 4 stated assumptions are product risks. "Do people enjoy collaborative puzzle games?" is like asking "Do you like having fun?" — the answer is always yes.
3. Prioritize: Warn the user that conversations cannot validate the core risks. Redirect toward the few market risks that exist: Are there enough puzzle game enthusiasts in this niche? What games do they currently play? How much do they spend on mobile games?
**Output (abbreviated):**
```
## Risk Overview
- Total risks identified: 8
- Market risks: 2 (audience size, spending habits)
- Product risks: 6 (fun factor, retention, multiplayer matchmaking, network effects, art quality, puzzle design)
- Biggest overlooked risk: Nearly all your risk is product-side. Customer conversations cannot tell you whether your game is fun. You need to build a prototype and watch people play.
### 1. Are there enough people who actively seek out collaborative puzzle games — and where do they congregate?
- Risk type: Market
- Why it matters: Even a great game fails if the target audience is too small or unfindable
- Scariness: Uncomfortable
- Prepared questions:
- "What puzzle games have you played in the last month? Tell me about the most recent session."
- "How do you discover new games? Walk me through the last game you downloaded."
- "Have you ever specifically searched for a game where you could play with friends?"
## Questions You Might Be Avoiding
- "Could I actually build a multiplayer puzzle game that is fun and retains players?" — This is your real risk, and conversations cannot answer it. Start prototyping.
```
---
### Scenario 3: Founder Updating Learning Goals After First Batch
**Trigger:** "I just finished 5 conversations about my invoice factoring tool for freelancers. I learned that freelancers definitely have cash flow problems (validated) and they mostly use spreadsheets to track invoices (validated). But I still don't know if they would trust a third party with their invoices, and I realize I never asked about pricing. What should I focus on next?"
**Process:**
1. Surface risks: Cash flow pain (validated), current tools (validated), trust with financial data (unvalidated), willingness to pay (unvalidated), plus hidden risks — do they invoice enough volume to justify a tool? Are there regulatory issues?
2. Classify: Trust and pricing are market risks. Invoice volume is market risk. Regulatory is mixed.
3. Prioritize top 3 for next batch:
- Goal 1: "Would freelancers trust a third-party service to handle their invoice payments?" (market risk, terrifying — deal-breaker if no)
- Goal 2: "How much money is stuck in late invoices per month, and what would they pay to get it faster?" (market risk, uncomfortable)
- Goal 3: "Do they invoice enough clients per month for factoring to be worthwhile?" (market risk, comfortable)
4. Note that the user explicitly identified they "never asked about pricing" — this was a scary question they avoided in the first batch
**Output (abbreviated):**
```
## Questions You Might Be Avoiding
- "Would you hand over control of your invoices to a service you found online?" — You avoided this in 5 conversations. That avoidance is a signal that this is your scariest and most important question.
- "What would you pay for this?" — You explicitly noted you avoided pricing. Ask about current spending on financial tools first, then explore willingness to pay.
```
## Key Principles
- **The questions you are avoiding are the ones you most need to ask** — Fear of bad news causes founders to ask comfortable questions that feel productive but do not de-risk anything. If you are not terrified of at least one question in every conversation, you are wasting the conversation. The cost of not asking is always higher than the cost of hearing a bad answer. One founder avoided asking lawyers about legal ambiguities and it cost half a million dollars.
- **Product risk and market risk require different validation methods** — When the customer says "If you can build it, I will pay," that is not validation — it is restating the obvious. Customer conversations validate market risk (Do they want it? Will they pay? Are there enough of them?). Product risk (Can I build it? Can I grow it? Will they keep using it?) requires building. Misclassifying your risk type leads to months of conversations that prove things nobody doubted.
- **Start broad before zooming in — always** — Most people have many problems they will happily discuss if you ask about them. Zooming into your specific problem area before confirming it is a top priority creates false validation. The person answers your detailed questions because you asked, not because they care. Start with "What are the big things you are trying to fix right now?" and only zoom in when they raise your area themselves. If they do not mention it unprompted, they probably do not care enough to pay for a solution.
- **Three learning goals is the right number** — Too few and each conversation covers too little ground. Too many and the conversation scatters across topics without going deep on any. Three goals lets you focus while remaining flexible enough to follow interesting threads. After each batch of conversations, drop answered goals and promote the next murkiest unknowns.
- **Lukewarm signals are more reliable than enthusiastic ones** — When someone says "That is pretty neat" or "I am not so sure about that," the instinct is to pitch harder until they say something nice. Resist this. A lukewarm response is perfectly reliable information — this person does not care enough. You cannot build a business on a lukewarm response. The only thing you gain from "convincing" them is a false positive.
## References
- For designing specific questions that pass customer conversation quality rules, use the `conversation-question-designer` skill
- For narrowing broad customer segments into specific, findable who-where pairs, use the `customer-segment-slicer` skill
- For the complete "does-this-problem-matter" diagnostic question set and risk classification details, see [risk-classification-guide.md](references/risk-classification-guide.md)
## License
This skill is licensed under [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/).
Source: [BookForge](https://github.com/bookforge-ai/bookforge-skills) — The Mom Test by Rob Fitzpatrick.
## Related BookForge Skills
This skill is standalone. Browse more BookForge skills: [bookforge-skills](https://github.com/bookforge-ai/bookforge-skills)
FILE:references/risk-classification-guide.md
# Risk Classification Guide
## Product Risk vs Market Risk
Every business faces risks in two fundamental categories. Correctly classifying your risks determines whether customer conversations are the right validation tool.
### Market Risk
**Definition:** Do they want it? Will they pay? Are there enough of them?
**Examples:**
- SaaS tools solving known pain points
- Services addressing recognized problems
- Products entering established categories with a differentiated approach
**Validation method:** Customer conversations are the primary tool. Ask about current behavior, workarounds, spending, and severity.
**Key signal:** When customers describe their current problem, their workarounds, and what they spend — that is market risk data you can act on.
### Product Risk
**Definition:** Can I build it? Can I grow it? Will they keep using it?
**Examples:**
- Video games (Is it fun? Will people play it repeatedly?)
- Marketplaces needing critical mass (Can you get enough supply AND demand?)
- Platforms needing network effects (Will users invite others?)
- Ad-supported models (Can you get enough traffic?)
- Technically complex products (Can the technology actually work?)
**Validation method:** You need to build something. Conversations can give you a starting point (understanding problem depth, confirming willingness to switch), but the core risk requires a prototype, beta, or proof of concept.
**Key signal:** When customer responses consistently sound like "Yes, if you can actually build/do that, I would definitely pay" — the risk is in the product, not the market. The customer is restating the obvious.
### Detection Test
Ask yourself: "Is the customer telling me something I did not already know, or are they confirming what everyone would say?"
- "Would you like more money?" — Everyone says yes. This validates nothing. Risk is product-side.
- "Would you switch trackers if something cheaper and more effective was available?" — Same as asking if they want more money. Obvious yes.
- "Would you pay if you could send customers on demand to your bar?" — Bars obviously want more customers. The risk is whether you can amass a consumer audience.
### Mixed Risk Situations
Most businesses have both types. Do not overlook either one.
**Farm fertility monitor example:** The founder spent 3 months on customer conversations asking farmers if they would switch to a better tracker. Farmers said "If you can build what you say, I will equip my whole herd." This sounded like validation but was actually product risk restated as enthusiasm. The real question: Can you build hardware that works reliably on farms?
**Nightclub app example:** Founders validated that bar owners want more customers (obvious) and consumers like cheap drinks (obvious). But the real risk — amassing enough users on both sides of the marketplace — was never tested through conversations.
## "Does-This-Problem-Matter" Diagnostic Questions
Use these questions to verify that a problem area is genuinely important to the person before zooming into details:
1. "How seriously do you take [area]?"
2. "Do you make money from it?"
3. "Have you tried making more money from it?"
4. "How much time do you spend on it each week?"
5. "Do you have any major aspirations for [area]?"
6. "Which tools and services do you use for it?"
7. "What are you already doing to improve this?"
8. "What are the 3 big things you are trying to fix or improve right now?"
These questions are generic by design. They give signals you can anchor on and dig around. The bulk of them are about finding out whether the person is taking this space seriously — are they spending money or making money? Is it in their top 3? Are they actively looking for solutions?
## Pre-Meeting Risk Discovery Questions
Two questions to unearth hidden risks before conversations:
1. **"If this company were to fail, why would it have happened?"** — Forces you to enumerate all failure modes, not just the one you find most interesting.
2. **"What would have to be true for this to be a huge success?"** — Surfaces the necessary conditions that you might be taking for granted.
These questions come from strategic planning (Lafley and Martin) and are useful both for the founding team during preparation and for guiding which risks to prioritize in conversations.
## The Premature Zoom Problem
### What It Is
Asking detailed questions about a specific problem area before confirming that area actually matters to the person. This creates data that looks like validation but is worthless.
### Why It Happens
Most people have lots of problems they do not actually care enough about to fix, but which they will happily tell you the details of if you ask. When you zoom in on your area immediately, you get detailed answers — not because the problem matters, but because you asked.
### How to Detect It
- Would the customer have raised this topic on their own?
- Are you assuming this problem area is important, or has the customer demonstrated importance through their behavior?
- If you asked "What are the 3 big things you are trying to fix right now?", would your area make their list?
### How to Fix It
Start broad: "What are your big goals and focuses right now?" Only zoom into your specific area when the customer independently raises it. If they do not mention it, it is probably not a top priority — and that is reliable, actionable information.
### The Fitness App Example
**Bad conversation:** Asks a non-exerciser about gym problems. Gets a ranking of fitness priorities. Concludes "we got a user!" — but the person never exercises and will never use the app.
**Good conversation:** Asks about life goals broadly. Fitness does not make the list. Conclusion: this person is not a customer. Moves on to find people who actually care about fitness enough to act on it.
The premature zoom is dangerous because if you are not paying attention, the bad conversation seems like it went well. You got detailed answers. You "validated" a problem. But you just led them there.
Audit a game, feature, live-ops layer, social system, or multiplayer concept for the quality and fit of its social design. Use when evaluating collaboration,...
--- name: game-design-multiplayer-feature-audit description: Audit a game, feature, live-ops layer, social system, or multiplayer concept for the quality and fit of its social design. Use when evaluating collaboration, competition, collaborate-to-compete structures, matchmaking, guilds/clubs, synchronous versus asynchronous play, realtime constraints, depth of social interaction, community formation, vanity/status systems, or how to add social play to a mostly single-player game. --- # Game Design Multiplayer Feature Audit Audit a design by asking what kind of social experience it is actually creating, for whom, at what coordination cost, and with what likely community effect. Use this skill when a design has multiplayer or social ambitions and you need to judge whether those ambitions are coherent, motivating, scalable, and well matched to the core fantasy of the game. ## Core principle Social design is not a checklist of features. A leaderboard, guild, chat channel, or PvP mode does not automatically create meaningful social play. Strong multiplayer design aligns player motivation, time structure, coordination demands, visibility, and community purpose. ## What to produce Generate: 1. **Audit target** - what is being reviewed and what kind of social experience it appears to aim for 2. **Social promise** - the core social fantasy or player promise 3. **Motivation map** - competition, collaboration, collaborate-to-compete, belonging, vanity/status, knowledge exchange 4. **Time and synchronization audit** - realtime, non-realtime, synchronous, asynchronous, or hybrid 5. **Social depth audit** - how deep the interaction really goes 6. **Community and status audit** - whether the system supports durable groups, identity, and readable prestige 7. **Risks / failure modes** - where the design is likely to break, flatten, or create friction 8. **Recommendations** - what to strengthen, stage, simplify, avoid, or postpone ## Process ### 1. Define the social promise State in one or two sentences what the feature is socially promising. Examples: - compete for rank and status against peers - cooperate with a small squad to solve hard encounters - contribute to a group goal while still pursuing personal goals - show off taste, city design, wealth, or mastery - let solo players feel the presence of others without hard coordination If the design appears to promise incompatible things at once, say so early. Common tension examples: - calm self-expression versus destructive PvP - casual mobile bursts versus rigid appointment play - individual authorship versus committee-driven collaboration ### 2. Map the motivation structure Audit the feature across these motivation buckets: - **Competition** - rivalry, ranking, domination, comparison - **Collaboration** - helping, supporting, coordinating, solving together - **Collaborate-to-compete** - teamwork in service of beating another team, club, faction, or cohort - **Belonging** - identity, membership, shared rituals, durable group attachment - **Vanity / status** - visible prestige, taste display, wealth display, proof of mastery - **Knowledge exchange** - teaching, strategy sharing, build discussion, optimization culture Do not just list them. Judge which ones are truly doing work and which are merely implied. ### 3. Check motivational fit Use a Self-Determination-Theory-inspired check: - **Autonomy** - does the player have choice of role, pace, route, or strategy? - **Competence** - can the player demonstrate mastery, improvement, contribution, or skill? - **Relatedness** - can the player meaningfully connect, compare, help, coordinate, or belong? Flag fake-social systems that mostly create obligation, admin work, or shallow compliance. Examples: - a guild donation button may create duty without meaningful relatedness - a giant anonymous global leaderboard may technically create comparison but fail emotionally - a cosmetic showcase may create status only if others can actually see and decode it ### 4. Audit time model and synchronization demands Classify the feature explicitly: - **Realtime synchronous** - **Non-realtime synchronous window** - **Asynchronous competitive** - **Asynchronous collaborative** - **Hybrid** Then ask: - how long does a typical social interaction last? - must players overlap in time? - what happens if one player misses the window? - does the audience/platform support this coordination burden? - is the feature mobile-friendly, session-friendly, or appointment-heavy? Call out time-model mismatch clearly. A socially appealing idea can still be wrong for the audience if it demands too much synchronization. ### 5. Audit depth of social interaction Rate the design using this depth ladder: 1. **Awareness** - others exist 2. **Comparison** - scores, rankings, visible collections, ghosts, showcases 3. **Indirect exchange** - gifting, trading, donations, borrowing 4. **Communication** - chat, pings, negotiation, requests 5. **Coordination** - timing, role division, tactical cooperation 6. **Collective strategy** - shared plans, doctrine, adaptation, team optimization 7. **Community identity** - durable groups, leadership, rituals, norms, reputation, belonging State where the feature sits now, where it wants to sit, and whether the gap is credible. Do not assume deeper is always better. More depth usually means more friction, moderation burden, onboarding cost, and design risk. ### 6. Audit competition design If the feature includes competition, evaluate: - leaderboard scale - intimacy of comparison group - freshness of score movement - reward brackets and goal density - fairness and matchmaking logic - anti-exploit / anti-smurf / anti-boost concerns - visibility of rivals and stakes - whether losing still feels legible and motivating Prefer emotionally legible comparison over giant anonymous ranking walls. Small groups, leagues, seasons, and visible rivals are often stronger than one global list. ### 7. Audit collaboration design If the feature includes cooperation, evaluate: - clarity of shared goal - role differentiation - visibility of contribution - whether casual or weaker players can still help - whether personal goals can also feed the group goal - dependency risk if one player flakes or churns - communication need versus communication tools provided Strong collaborative systems often let players pursue personal goals that still contribute to a shared outcome. ### 8. Audit community formation Ask whether the design supports durable social structure: - clubs, clans, guilds, alliances, squads - friend discovery and invitations - reasons to stay in a group - recurring cadence and rituals - visible contribution and group memory - discoverability of healthy groups - lightweight leadership roles or responsibilities Ask the blunt question: **Why would a player bother joining or maintaining this group?** If the answer is only chat access, habit, or raw rewards, call that out as thin. ### 9. Audit vanity and status Evaluate the status layer through four checks: 1. **Visibility** - can other players see the signal? 2. **Legibility** - can they understand what it means? 3. **Desirability** - is it aspirational? 4. **Fairness** - what exactly is being signaled: skill, taste, effort, money, tenure, luck? Common status surfaces: - ranks and leagues - trophies and seasonal records - rare cosmetics - city/base/avatar/profile display - titles and badges - featured placements and judged showcases Vanity systems are weak when they are private, unreadable, or disconnected from any real social surface. ### 10. Audit fit for mostly single-player games When the design adds social play to a mostly solo experience, ask: - what player behaviors already suggest social demand? - what social fantasy naturally fits the core loop? - what part of the fantasy should remain personal and unshared? - should the first social layer be comparison, exchange, clubs, judged showcases, or direct PvP? - is the design trying to force deep collaboration onto a fantasy built around individual authorship or control? Be skeptical of bolted-on realtime multiplayer when the core fantasy is solitary mastery, self-expression, or authorship. A safer migration path often goes: 1. observe others 2. compare with others 3. exchange with others 4. group with others 5. collaborate to compete 6. only then add tightly synchronized modes if the audience proves it wants them ### 11. Diagnose failure patterns Common failure shapes: - **social wallpaper** - many social features, little actual social meaning - **coordination overkill** - audience asked for more synchronization than it can sustain - **status fog** - prestige exists but players cannot read or value it - **guild shell** - groups exist but have no real purpose - **comparison numbness** - ranks exist but movement feels emotionally meaningless - **solo fantasy violation** - the social layer damages the core fantasy instead of extending it - **high-friction collaboration** - teamwork exists but the cost of organizing it is too high - **shallow relatedness** - players are adjacent, not meaningfully connected ### 12. Convert findings into actions For each major issue, specify: - **Issue** - **Why it hurts** - **What kind of player it hurts most** - **Suggested change** - **Expected effect** ## Response structure ### Audit Target - ... ### Social Promise - ... ### Motivation Map - Competition: ... - Collaboration: ... - Collaborate-to-compete: ... - Belonging: ... - Vanity / status: ... - Knowledge exchange: ... ### Time and Synchronization Audit - ... ### Social Depth Audit - Current depth: ... - Intended depth: ... - Gap: ... ### Community and Status Audit - ... ### Risks / Failure Modes 1. ... 2. ... 3. ... ### Recommendations - Do now: ... - Do later: ... - Avoid: ... ## Fast mode Use this quick pass when speed matters: - what social fantasy is this actually selling? - is it mostly competition, collaboration, or collaborate-to-compete? - does the time model fit the audience? - how deep is the social interaction really? - why would players stay in a group? - what visible status or prestige does the system create? - what is the single biggest mismatch or risk? ## References Read these when useful: - `references/social-design-dimensions.md` for the deeper multiplayer audit checklist and sharper prompts ## Working principle Strong multiplayer design does not merely place players near each other. It creates meaningful comparison, contribution, coordination, recognition, or belonging at a coordination cost the audience is actually willing to pay. FILE:references/social-design-dimensions.md # Social design dimensions Use this file when the user wants a denser rubric, sharper prompts, or a more systematic multiplayer audit. ## 1. Motivation matrix For each feature, score low / medium / high and justify briefly. - **Competition** - compare, beat, rank above, dominate - **Collaboration** - help, support, solve together - **Collaborate-to-compete** - coordinate within a group to defeat another group or cohort - **Belonging** - identity, attachment, membership, shared ritual - **Status** - prestige others can read - **Vanity** - taste, wealth, authorship, or style on display - **Knowledge exchange** - guides, strategy sharing, doctrine, teaching Ask: - which motivation is actually carrying the feature? - which is merely cosmetic? - which player segment is most likely to care? ## 2. Time and coordination rubric Check: - minimum players needed - overlap in time required or not required - session length expectation - latency sensitivity - whether the feature is burst-friendly or appointment-heavy - what happens when one participant misses a step - whether the feature supports 2-minute, 10-minute, and 30-minute participation bands Warning signs: - mobile audience asked to coordinate like a raid team - one absent player collapses the whole structure - feature marketed as casual but behaves like shift work - social reward requires too much scheduling overhead ## 3. Social depth prompts Ask: - are players merely visible to each other, or meaningfully affecting each other? - can they exchange value? - can they express intent? - can they negotiate or request help? - can they divide labor? - can they build trust, reputation, or norms over time? - can they admire, envy, or learn from each other? ## 4. Competition prompts Evaluate: - scale of comparison group - freshness of movement on the board - fairness of matchmaking or seeding - reward ladder and aspiration structure - whether top-end status is reachable, reset, or permanently locked away - whether the format creates meaningful rivals - whether failure teaches or only humiliates Typical fixes: - shrink comparison groups - add leagues or brackets - shorten round cadence - increase score feedback frequency - make immediate rivals more visible ## 5. Collaboration prompts Evaluate: - clarity of shared goal - role differentiation - contribution visibility - tolerance for absences or churn - whether weak/casual players can still help - whether veterans can mentor newer players - whether communication tools match the coordination problem Typical fixes: - let personal progress also feed group progress - reduce exact coordination requirements - show contribution clearly - remove single points of failure ## 6. Community prompts Check: - onboarding into groups - reasons to remain in a group - rituals, cadence, recurring events - group history, memory, or identity - discoverability of good groups - leadership roles, social hierarchy, governance - moderation, abuse handling, conflict risk Strong signs: - players return because of the people, not only the reward - groups produce stories, traditions, doctrine, or shared identity - contribution is visible and socially acknowledged ## 7. Vanity and status prompts Check whether the design supports: - **taste display** - decoration, city/base layout, loadout curation, composition - **mastery display** - difficult cosmetics, titles, ratings, trophies - **tenure display** - season history, legacy badges, old-event proof - **wealth display** - rare items, premium cosmetics, abundance signals - **social proof** - likes, endorsements, featured placement, group rank Failure modes: - players cannot see the signal - players see it but cannot decode it - everything is monetized, so nothing means much - status loops humiliate the majority without offering reachable aspiration ## 8. Single-player-to-social migration prompts Use when a game is adding social layers after launch or onto a mostly solo concept. Ask: - what emergent social behavior already exists outside the game? - what are players already comparing, sharing, trading, or showing off? - what part of the fantasy is too personal to turn into committee play? - is direct PvP actually needed? - would judged showcases, leagues, exchange, or club goals fit better first? Safer migration pattern: 1. awareness and visitation 2. comparison and judged display 3. exchange and trade 4. groups and identity 5. collaborate-to-compete 6. tightly synchronized modes only if proven necessary
5GC Web仪表自动化技能,支持AMF/UDM/AUSF/SMF/PGW-C/UPF/PGW-U/GNB/UE/PCF/NRF/QoS/TC/PCC/smpolicy的批量添加与编辑及PCF默认规则一键配置
---
name: 5gc-web-dotouch
version: 1.0.0
description: 5GC Web仪表自动化技能,支持AMF/UDM/AUSF/SMF/PGW-C/UPF/PGW-U/GNB/UE/PCF/NRF/QoS/TC/PCC/smpolicy的批量添加与编辑及PCF默认规则一键配置
author: liuwei120
tags: [5gc, automation, playwright, network]
---
# 5GC Web 仪表自动化技能
> 统一管理 AMF、UDM/AUSF、SMF/PGW-C、UPF/PGW-U、GNB、UE、PCF、NRF 八类网元的添加与编辑操作,以及 PCC 规则、QoS 模板、Traffic Control、SMPolicy 和 PCF 默认规则一键配置。
---
## 目录
- [快速开始](#快速开始)
- [统一 CLI 入口](#统一-cli-入口)
- [技能详细文档](#技能详细文档)
- [AMF](#amf)
- [UDM/AUSF](#udmausf)
- [SMF/PGW-C](#smfpgw-c)
- [UPF/PGW-U](#upfpgw-u)
- [GNB](#gnb)
- [UE](#ue)
- [PCF/PCRF](#pcfpcrf)
- [PCC 规则](#pcc-规则)
- [QoS 模板](#qos-模板)
- [Traffic Control](#traffic-control)
- [SMPolicy](#smpolicy)
- [UE Smpolicy](#smpolicy-ue-add-skilljs)
- [DNN Smpolicy](#smpolicy-dnn)
- [DNN Smpolicy](#smpolicy-dnn)
- [TAC Smpolicy](#smpolicy-tac)
- [Cell Smpolicy](#smpolicy-cell)
- [Cell Forbidden Smpolicy](#smpolicy-cell-forbidden)
- [NRF](#nrf)
- [全局参数参考](#全局参数参考)
- [字段参考](#字段参考)
---
## 快速开始
### 安装方法
技能目录位于 `skills/5gc/`,由统一入口 `5gc.js` 统一调度,无需额外安装:
```bash
# 克隆或复制到本机
git clone <repo> ~/.openclaw/workspace/skills/5gc
# 直接使用统一入口(推荐)
node skills/5gc/scripts/5gc.js <entity> <action> [options]
# 或直接调用各脚本
node skills/5gc/scripts/amf-add-skill.js <参数>
```
### 前置要求
- Node.js ≥ 14
- Playwright(`npm i playwright && npx playwright install chromium`)
- 5GC 仪表地址:`https://192.168.3.89`(默认)
- 登录凭证:`[email protected]` / `dotouch`
- 仪表上已创建对应工程(如 `XW_S5GC_1`)
### 会话缓存
所有脚本自动复用 Playwright 会话缓存(`.sessions/` 目录),首次登录后再次运行无需重复登录。
---
## 统一 CLI 入口
### 路径
```
node skills/5gc/scripts/5gc.js <entity> <action> [options]
```
### 支持的网元与操作
| entity | add | edit | 特殊操作 |
|--------|-----|------|---------|
| `amf` | ✅ | ✅ | |
| `udm` | ✅ | ✅ | |
| `smf` | ✅ | ✅ | |
| `upf` | ✅ | ✅ | |
| `gnb` | ✅ | ✅ | |
| `ue` | ✅ | ✅ | |
| `pcf` | ✅ | ✅ | `default-rule-add` |
| `pcc` | ✅ | ✅ | |
| `qos` | ✅ | | |
| `tc` | ✅ | | |
| `smpolicy` | | | `add-pcc`, `ue-add`, `ue-edit`, `dnn-add`, `dnn-edit` |
| `nrf` | ✅ | ✅ | |
### 全局选项
| 选项 | 说明 |
|------|------|
| `--url <地址>` | 5GC 仪表地址,默认 `https://192.168.3.89` |
| `--headed` | 打开可见浏览器窗口(调试用) |
### 三种使用模式
```bash
# 1. 添加网元
node 5gc.js amf add <名称> [参数...]
# 2. 批量编辑(当前工程下所有该类网元)
node 5gc.js amf edit --project <工程> --set-<字段> <值>
# 3. 单个编辑(按名称精确匹配)
node 5gc.js amf edit --name <名称> --project <工程> --set-<字段> <值>
```
---
## 技能详细文档
---
### AMF
#### amf-add-skill.js
**功能**:在指定工程下添加一个 AMF 实例。
**使用方式**:
```bash
node 5gc.js amf add <名称> [选项...]
# 或直接调用
node skills/5gc/scripts/amf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | AMF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `5G_basic_process` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--mcc <值>` | MCC(移动国家码) | `460` |
| `--mnc <值>` | MNC(移动网络码) | `01` |
| `--ngap_sip <IP>` | NGAP 信令面 IP | `200.20.20.1` |
| `--ngap_port <端口>` | NGAP 端口 | `38412` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `200.20.20.5` |
| `--http2_port <端口>` | HTTP2 端口 | `8080` |
| `--stac <值>` | 起始 TAC | `101` |
| `--etac <值>` | 结束 TAC | `102` |
| `--region_id <值>` | 区域 ID | `1` |
| `--set_id <值>` | Set ID | `1` |
| `--pointer <值>` | 指针 | `1` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
# 基本添加
node 5gc.js amf add AMF_TEST --project XW_S5GC_1
# 指定 NGAP IP 和端口
node 5gc.js amf add AMF_PROD --project XW_S5GC_1 --ngap_sip 10.200.1.50 --ngap_port 38412
# 使用不同 MCC/MNC
node 5gc.js amf add AMF_CMCC --project XW_S5GC_1 --mcc 460 --mnc 00
```
---
#### amf-edit-skill.js
**功能**:修改 AMF 配置参数。支持单个修改或批量修改工程下所有 AMF。
**使用方式**:
```bash
node 5gc.js amf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` / `-p <工程>` | 目标工程,不带 `--name` 时批量修改该工程下所有 AMF |
| `--name <名称>` | 精确匹配要修改的 AMF 名称 |
| `--id <ID>` | 按 AMF ID 修改 |
| `--set-<字段> <值>` | 修改指定字段的值,支持多组 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**可编辑字段**:`name`, `mcc`, `mnc`, `ngap_sip`, `ngap_port`, `http2_sip`, `http2_port`, `stac`, `etac`, `region_id`, `set_id`, `pointer`, `ea[NEA0]`, `ea[128-NEA1]`, `ea[128-NEA2]`, `ea[128-NEA3]`, `ia[NIA0]`, `ia[128-NIA1]`, `ia[128-NIA2]`, `ia[128-NIA3]`
> ⚠️ `ea[NEA0]` 等算法字段:实际向表单填入字段名 `ea[NEA0]`(input[name="ea[NEA0]"]),layui checkbox 点击基于索引而非字段名,详情见 SKILL.md 算法配置章节。
**示例**:
```bash
# 批量修改工程下所有 AMF 的 NGAP IP
node 5gc.js amf edit --project XW_S5GC_1 --set-ngap_sip 10.200.1.99
# 修改指定 AMF
node 5gc.js amf edit --name AMF_TEST --project XW_S5GC_1 --set-ngap_sip 10.200.1.50 --set-http2_sip 10.200.1.51
# 按 ID 修改
node 5gc.js amf edit --id 6633 --set-ngap_port 38413
```
---
### UDM/AUSF
#### ausf-udm-add-skill.js
**功能**:在指定工程下添加一个 UDM/AUSF 实例。
**使用方式**:
```bash
node 5gc.js udm add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | UDM 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `5G_basic_process` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--count <数量>` | 实例数量 | `1` |
| `--sip <IP>` | SIP 服务 IP | `192.168.20.30` |
| `--port <端口>` | SIP 端口 | `80` |
| `--auth_method <方法>` | 认证方法 | `5G_AKA` |
| `--scheme <协议>` | 协议类型 | `HTTP` |
| `--priority <优先级>` | 优先级 | `8` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
# 基本添加
node 5gc.js udm add UDM_TEST --project XW_S5GC_1
# 指定 SIP IP 和端口
node 5gc.js udm add UDM_PROD --project XW_S5GC_1 --sip 10.0.0.100 --port 8080
# 批量添加 3 个实例
node 5gc.js udm add UDM_CLUSTER --project XW_S5GC_1 --count 3 --sip 10.0.0.50
```
---
#### ausf-udm-edit-skill.js
**功能**:修改 UDM/AUSF 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js udm edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 UDM 名称 |
| `--set-sip <IP>` | 修改 SIP IP |
| `--set-port <端口>` | 修改端口 |
| `--set-auth_method <方法>` | 修改认证方法 |
| `--set-scheme <协议>` | 修改协议 |
| `--set-count <数量>` | 修改实例数量 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**示例**:
```bash
# 批量修改工程下所有 UDM 的 SIP IP
node 5gc.js udm edit --project XW_S5GC_1 --set-sip 10.0.0.99
# 修改指定 UDM
node 5gc.js udm edit --name UDM_TEST --project XW_S5GC_1 --set-sip 10.0.0.88 --set-port 8080
```
---
### SMF/PGW-C
#### smf-pgwc-add-skill.js
**功能**:在指定工程下添加一个 SMF/PGW-C 实例。
**使用方式**:
```bash
node 5gc.js smf add --name <名称> [选项...]
```
> ⚠️ 通过 5gc.js 统一调度时必须使用 `--name <名称>` 形式(不是位置参数)。
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--name <名称>` | SMF 实例名称 | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--pfcp_sip <IP>` | PFCP 信令面 IP | `200.20.20.25` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `200.20.20.25` |
| `--mcc <值>` | MCC | `460` |
| `--mnc <值>` | MNC | `01` |
| `--pdu_capacity <数量>` | PDU 会话容量 | `200000` |
| `--ue_min <IP>` | UE IP 池起始 | `30.30.30.20` |
| `--ue_max <IP>` | UE IP 池结束 | `30.31.30.20` |
| `--interest_tac <TAC列表>` | 关注 TAC 列表(逗号分隔) | `101,102` |
| `--headed` | 打开可见浏览器 | false |
> ✅ **NSSAI 自动配置**:脚本在 SMF 创建后会自动打开 NSSAI 配置弹窗,添加一条默认 NSSAI(SST=1, SD=000001, DNN Group=cscn2net)。如需自定义 NSSAI 参数,请直接修改脚本中的硬编码值。
>
> ⚠️ ue_sip6 / ue_eip6 为硬编码值,不支持 CLI 参数覆盖。
**示例**:
```bash
# 基本添加
node 5gc.js smf add --name SMF_TEST --project XW_S5GC_1
# 指定工程和 IP/MCC
node 5gc.js smf add --name SMF_PROD --project XW_S5GC_1 --pfcp_sip 10.10.10.50 --http2_sip 10.10.10.51 --mcc 460 --mnc 01
```
---
#### smf-pgwc-edit-skill.js
**功能**:修改 SMF/PGW-C 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js smf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 SMF 名称 |
| `--set-pfcp_sip <IP>` | 修改 PFCP 信令面 IP |
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-pdu_capacity <数量>` | 修改 PDU 会话容量 |
| `--set-ue_min <IP>` | 修改 UE IP 池起始 |
| `--set-ue_max <IP>` | 修改 UE IP 池结束 |
| `--set-interest_tac <TAC列表>` | 修改关注 TAC 列表(逗号分隔) |
> ⚠️ 以下字段不支持 `--set-` 修改:dnn、n3_ip、n6_ip、snssai_sst、snssai_sd。如需修改,请通过仪表 UI 手动完成。NSSAI 配置请在添加时自动完成(见上文)。
**示例**:
```bash
# 批量修改工程下所有 SMF 的 HTTP2 IP
node 5gc.js smf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 SMF 的 pfcp_sip 和 MCC/MNC
node 5gc.js smf edit --name SMF_TEST --project XW_S5GC_1 --set-pfcp_sip 10.10.10.88 --set-mcc 460 --set-mnc 01
```
---
### UPF/PGW-U
#### upf-add-skill.js
**功能**:在指定工程下添加一个 UPF/PGW-U 实例。
**使用方式**:
```bash
node 5gc.js upf add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | UPF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--n4_ip <IP>` | N4 接口 IP | `192.168.20.30` |
| `--n3_ip <IP>` | N3 接口 IP | `192.168.20.30` |
| `--n6_ip <IP>` | N6 接口 IP | `192.168.20.31` |
| `--n4_port <端口>` | N4 端口 | `8805` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--pdu_capacity <数量>` | PDU 会话容量 | `20000` |
| `--ue_min <IP>` | UE IP 池起始 | `20.20.20.20` |
| `--ue_max <IP>` | UE IP 池结束 | `20.20.60.20` |
| `--headed` | 打开可见浏览器 | false |
> ⚠️ DNN、TAC、NSSAI 在添加脚本中为硬编码默认值,不支持命令行覆盖。如需修改,请使用 `upf edit` 脚本。
**示例**:
```bash
# 基本添加
node 5gc.js upf add UPF_TEST --project XW_S5GC_1
# 指定 N4/N3/N6 IP 和 MCC/MNC
node 5gc.js upf add UPF_PROD --project XW_S5GC_1 --n4_ip 10.0.0.50 --n6_ip 10.0.0.51 --MCC 460 --MNC 01
```
---
#### upf-edit-skill.js
**功能**:修改 UPF/PGW-U 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js upf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 UPF 名称 |
| `--set-n3_ip <IP>` | 修改 N3 接口 IP |
| `--set-n4_ip <IP>` | 修改 N4 接口 IP |
| `--set-n4_port <端口>` | 修改 N4 端口 |
| `--set-n6_ip <IP>` | 修改 N6 接口 IP |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
| `--set-pdu_capacity <数量>` | 修改 PDU 会话容量 |
| `--set-ue_min <IP>` | 修改 UE IP 池起始 |
| `--set-ue_max <IP>` | 修改 UE IP 池结束 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
> ⚠️ `dnn`(DNN)和 TAC/NSSAI 在 UPF 表单中存储在 jsgrid 配置行内,不支持简单的 `--set-` 修改。
**示例**:
```bash
# 批量修改工程下所有 UPF 的 N4 IP
node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 99.99.99.99
# 修改指定 UPF 的 N4/N6 IP 和 MCC/MNC
node 5gc.js upf edit --name UPF_TEST --project XW_S5GC_1 --set-n4_ip 88.88.88.88 --set-n6_ip 88.88.88.89 --set-MCC 460 --set-MNC 01
```
---
### GNB
#### gnb-add-skill.js
**功能**:在指定工程下添加一个 GNB 实例。
**使用方式**:
```bash
node 5gc.js gnb add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | GNB 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--ngap_sip <IP>` | NGAP 信令面 IP | `200.20.20.50` |
| `--user_sip_ip_v4 <IP>` | 用户面 IPv4 | `2.2.2.2` |
| `--mcc <值>` | MCC | `460` |
| `--mnc <值>` | MNC | `60` |
| `--stac <值>` | 起始 TAC | `0` |
| `--etac <值>` | 结束 TAC | `0` |
| `--node_id <ID>` | 节点 ID | `70` |
| `--cell_count <数量>` | 小区数量 | `1` |
| `--headed` | 打开可见浏览器 | false |
> ⚠️ `stac`/`etac`/`node_id` 非默认值时可能触发表单验证失败,建议先使用默认值添加后再用 `gnb edit` 修改。
**示例**:
```bash
# 基本添加
node 5gc.js gnb add GNB_TEST --project XW_S5GC_1
# 指定 NGAP IP、用户面 IP 和 TAC
node 5gc.js gnb add GNB_PROD --project XW_S5GC_1 --ngap_sip 200.20.20.100 --user_sip_ip_v4 3.3.3.3 --mcc 460 --mnc 60 --stac 1 --etac 10
```
---
#### gnb-edit-skill.js
**功能**:修改 GNB 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js gnb edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 GNB 名称 |
| `--set-ngap_sip <IP>` | 修改 NGAP 信令面 IP |
| `--set-user_sip_ip_v4 <IP>` | 修改用户面 IPv4 |
| `--set-user_sip_ip_v6 <IP>` | 修改用户面 IPv6 |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-stac <值>` | 修改起始 TAC |
| `--set-etac <值>` | 修改结束 TAC |
| `--set-node_id <ID>` | 修改节点 ID |
| `--set-cell_count <数量>` | 修改小区数量 |
| `--set-replay_ip <IP>` | 修改回放 IP |
| `--set-replay_port <端口>` | 修改回放端口 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**示例**:
```bash
# 批量修改工程下所有 GNB 的用户面 IP
node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 99.99.99.99
# 修改指定 GNB 的 NGAP IP 和 MCC/MNC
node 5gc.js gnb edit --name GNB_TEST --project XW_S5GC_1 --set-ngap_sip 200.20.20.88 --set-mcc 461 --set-mnc 22
```
---
### UE
#### ue-add-skill.js
**功能**:在指定工程下添加一个或多个 UE 实例。
**使用方式**:
```bash
node 5gc.js ue add --name <名称> [选项...]
```
**参数**:
| 参数 | 短名 | 说明 | 默认值 |
|------|------|------|--------|
| `--name <名称>` | `-n <名称>` | UE 名称(只支持字母/数字/下划线) | **必填** |
| `--project <工程>` | `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | `-u <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--imsi <值>` | | 起始 IMSI(15位) | `460001234567890` |
| `--msisdn <值>` | | MSISDN(13-15位,以 86 开头) | `8611111111111` |
| `--mcc <值>` | | MCC | `460` |
| `--mnc <值>` | | MNC | `01` |
| `--key <值>` | | KI 密钥(32位 hex) | `1111...`(32个1) |
| `--opc <值>` | | OPc 密钥(32位 hex) | `1111...`(32个1) |
| `--imeisv <值>` | | IMEISV(偶数位) | `8611111111111111` |
| `--sst <值>` | | NSSAI SST | `1` |
| `--sd <值>` | | NSSAI SD | `111111` |
| `--count <数量>` | `-c <数量>` | 连续添加数量 | `1` |
| `--headed` | | 打开可见浏览器 | false |
> **命名约束**:UE 名称只能包含字母、数字、下划线(`_`),不能使用连字符(`-`)或其他特殊字符。
**示例**:
```bash
# 基本添加
node 5gc.js ue add --name UE_001 --project XW_S5GC_1
# 指定 IMSI 和 MSISDN
node 5gc.js ue add --name UE_TEST --imsi 460000000000001 --msisdn 8613888888888 --project XW_S5GC_1
# 批量添加 10 个连续 UE
node 5gc.js ue add --name UE_BATCH --count 10 --project XW_S5GC_1 --msisdn 8613900000000
# 指定认证密钥
node 5gc.js ue add --name UE_AUTH --project XW_S5GC_1 --key 00112233445566778899aabbccddeeff --opc 11223344556677889900aabbccddeeff
```
---
#### ue-edit-skill.js
**功能**:修改 UE 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js ue edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改该工程下所有 UE |
| `--name <名称>` | 精确匹配要修改的 UE 名称(不支持批量时按名称过滤) |
| `--id <ID>` | 按 UE ID 修改 |
| `--set-msisdn <值>` | 修改 MSISDN |
| `--set-s_imsi <值>` | 修改 IMSI |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-key <值>` | 修改 KI 密钥 |
| `--set-opc <值>` | 修改 OPc 密钥 |
| `--set-imeisv <值>` | 修改 IMEISV |
| `--set-sst <值>` | 修改 NSSAI SST |
| `--set-sd <值>` | 修改 NSSAI SD |
| `--set-replay_ip <IP>` | 修改回放 IP |
| `--set-replay_port <端口>` | 修改回放端口 |
| `--set-count <数量>` | 修改数量 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
> ⚠️ `user_sip_ip_v4`、`user_sip_ip_v6` 在 UE 编辑表单中不存在此字段名,无需修改。
**示例**:
```bash
# 批量修改工程下所有 UE 的 MSISDN
node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613911111111
# 修改指定 UE
node 5gc.js ue edit --name UE_001 --project XW_S5GC_1 --set-msisdn 8613988888888 --set-sst 1 --set-sd 222222
# 按 ID 修改
node 5gc.js ue edit --id 10337 --set-opc aabbccddeeff00112233445566778899 --set-imeisv 8611111111111112
```
---
### PCF/PCRF
#### pcf-add-skill.js
**功能**:在指定工程下添加一个 PCF/PCRF 实例。
**使用方式**:
```bash
node 5gc.js pcf add <名称> [选项...]
node skills/5gc/scripts/pcf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | PCF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `192.168.20.90` |
| `--http2_port <端口>` | HTTP2 端口 | `80` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
node 5gc.js pcf add PCF-TEST --project XW_S5GC_1
node 5gc.js pcf add PCF-PROD --project XW_S5GC_1 --http2_sip 10.0.0.50 --MCC 460 --MNC 01
```
#### pcf-edit-skill.js
**功能**:编辑指定工程下的 PCF/PCRF 实例(支持单条和批量)。
**使用方式**:
```bash
# 批量编辑:修改工程下所有 PCF 的字段
node 5gc.js pcf edit --project <工程> --set-<字段> <值>
# 单条编辑:修改指定名称的 PCF
node 5gc.js pcf edit --name <名称> --project <工程> --set-<字段> <值>
```
**可编辑字段**:
| 参数 | 说明 |
|------|------|
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-http2_port <端口>` | 修改 HTTP2 端口 |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
**示例**:
```bash
# 批量修改工程下所有 PCF 的 HTTP2 IP
node 5gc.js pcf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 PCF 的 HTTP2 IP 和 MNC
node 5gc.js pcf edit --name pcc --project XW_S5GC_1 --set-http2_sip 10.10.10.88 --set-MNC 01
```
#### default-rule-add-skill.js(PCF 默认规则一键配置)
**功能**:为指定工程一键配置完整的 PCF 默认规则链路,包含 QoS 模板 → Traffic Control → PCC 规则 → sm_policy_default → PCF default_smpolicy 全五步。
**使用方式**:
```bash
node 5gc.js pcf default-rule-add --project <工程> [选项...]
node skills/5gc/scripts/default-rule-add-skill.js --project <工程> [选项...]
```
**参数**(全部可选,有默认值):
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--qos-id` | QoS 模板 ID | `qos_default_{时间戳}` |
| `--5qi` | 5QI 值(不指定则自动选择未使用的值) | 自动(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率 | `10000000` |
| `--maxbr-dl` | 下行最大比特率 | `20000000` |
| `--gbr-ul` | 上行保证比特率 | `5000000` |
| `--gbr-dl` | 下行保证比特率 | `5000000` |
| `--tc-id` | TC 规则 ID | `tc_default_{时间戳}` |
| `--flow-status` | TC 流状态 | `ENABLED` |
| `--pcc-id` | PCC 规则 ID | `pcc_default` |
| `--precedence` | PCC 优先级 | `63` |
| `--headed` | 显示浏览器窗口(调试用) | off |
**示例**:
```bash
# 最简用法(自动生成所有 ID)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4
# 指定 QoS 参数(高速率)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 \
--qos-id qos_high_rate --5qi 8 \
--maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 指定 PCC 优先级
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --precedence 50
# 调试模式
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --headed
```
> **注意**:同一工程多次运行会自动删除旧的同名资源并重建,不会污染配置。
### PCC 规则
#### pcc-add-skill.js
**功能**:在指定工程下添加一条 PCC 规则(PCC 规则用于绑定 QoS 模板和 Traffic Control)。
**使用方式**:
```bash
node 5gc.js pcc add --project <工程> --pcc-id <ID> --qos <QoS名称> --tc <TC名称> [选项...]
node skills/5gc/scripts/pcc-add-skill.js --project <工程> --pcc-id <ID> --qos <QoS名称> --tc <TC名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--pcc-id` | PCC 规则 ID(字母/数字/下划线) | **必填** |
| `--qos` | 引用的 QoS 模板名称 | **必填** |
| `--tc` | 引用的 Traffic Control 名称 | **必填** |
| `--precedence` | 优先级(0-255) | `63` |
| `--flow-desc` | 流描述(可选) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
# 指定优先级
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_high --qos qos2 --tc tc1 --precedence 50
```
#### pcc-edit-skill.js
**功能**:编辑已有 PCC 规则的 QoS/TC 绑定(切换 PCC 引用的 QoS 模板或 Traffic Control)。
**使用方式**:
```bash
node 5gc.js pcc edit --project <工程> --name <PCC名称> --set-qos <新QoS> [--set-tc <新TC>]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要修改的 PCC 规则名称(精确匹配) |
| `--set-qos <名称>` | 切换到新的 QoS 模板 |
| `--set-tc <名称>` | 切换到新的 Traffic Control |
| `--headed` | 显示浏览器窗口 |
**示例**:
```bash
# 修改 PCC 引用的 QoS(用于修改上下行速率)
node 5gc.js pcc edit --project XW_SUPF_5_1_2_4 --name pcc_default --set-qos qos_high_rate
# 同时修改 QoS 和 TC
node 5gc.js pcc edit --project XW_SUPF_5_1_2_4 --name pcc_default --set-qos qos1 --set-tc tc2
```
> ⚠️ **重要**:PCC 的 `refQosData` 和 `refTcData` 存储在 xm-select 多选组件中。编辑时会自动切换选择,无需手动取消旧选项。
### NRF(网络存储功能)
#### nrf-add-skill.js
**功能**:在指定工程下添加一个 NRF 实例。
**使用方式**:
```bash
node 5gc.js nrf add <名称> [选项...]
node skills/5gc/scripts/nrf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | NRF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `192.168.20.100` |
| `--http2_port <端口>` | HTTP2 端口 | `80` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
node 5gc.js nrf add NRF-TEST --project XW_S5GC_1
node 5gc.js nrf add NRF-PROD --project XW_S5GC_1 --http2_sip 10.0.0.50 --MCC 460 --MNC 01
```
#### nrf-edit-skill.js
**功能**:编辑指定工程下的 NRF 实例(支持单条和批量)。
**使用方式**:
```bash
# 批量编辑:修改工程下所有 NRF 的字段
node 5gc.js nrf edit --project <工程> --set-<字段> <值>
# 单条编辑:修改指定名称的 NRF
node 5gc.js nrf edit --name <名称> --project <工程> --set-<字段> <值>
```
**可编辑字段**:
| 参数 | 说明 |
|------|------|
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-http2_port <端口>` | 修改 HTTP2 端口 |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
**示例**:
```bash
# 批量修改工程下所有 NRF 的 HTTP2 IP
node 5gc.js nrf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 NRF 的 HTTP2 IP 和 MNC
node 5gc.js nrf edit --name nrf1 --project XW_S5GC_1 --set-http2_sip 10.10.10.88 --set-MNC 01
```
### QoS 模板
#### qos-add-skill.js
**功能**:在指定工程下添加一个 QoS(服务质量)模板,定义 5QI、上下行最大比特率、保证比特率等参数。
**使用方式**:
```bash
node 5gc.js qos add --project <工程> --qos-id <ID> [选项...]
node skills/5gc/scripts/qos-add-skill.js --project <工程> --qos-id <ID> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--qos-id` | QoS 模板 ID(字母/数字/下划线) | **必填** |
| `--5qi` | 5QI 值(不指定则自动选择) | 自动选择未使用的值(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率(bps) | `10000000` |
| `--maxbr-dl` | 下行最大比特率(bps) | `20000000` |
| `--gbr-ul` | 上行保证比特率(bps) | `5000000` |
| `--gbr-dl` | 下行保证比特率(bps) | `5000000` |
| `--priority` | 优先级 | 空 |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos1
# 高速率 QoS
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_high \
--5qi 8 --maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 批量创建不同 5qi 的 QoS 模板
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_6 --5qi 6
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_9 --5qi 9
```
---
### Traffic Control
#### tc-add-skill.js
**功能**:在指定工程下添加一条 Traffic Control 流量控制规则,控制 UE 流量的启用/禁用状态。
**使用方式**:
```bash
node 5gc.js tc add --project <工程> --tc-id <ID> [选项...]
node skills/5gc/scripts/tc-add-skill.js --project <工程> --tc-id <ID> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--tc-id` | TC 规则 ID(字母/数字/下划线) | **必填** |
| `--flow-status` | 流状态 | `ENABLED` |
| `--flow-desc` | 流描述(可选) | |
| `--headed` | 显示浏览器窗口 | off |
> **flow-status 选项**:`ENABLED`(启用)、`DISABLED`(禁用)、`ENABLED-UPLINK`(仅上行)等。
**示例**:
```bash
# 基本添加
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc1
# 指定流状态
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_uplink --flow-status ENABLED-UPLINK
```
---
### SMPolicy
#### smpolicy_add_pcc.js {#smpolicy-default}
**功能**:将已有 PCC 规则添加到工程默认的 `sm_policy_default` 会话策略中(追加到 pccRules 列表)。
**使用方式**:
```bash
node 5gc.js smpolicy add-pcc --project <工程> --pcc-id <PCC名称>
node skills/5gc/scripts/smpolicy_add_pcc.js --project <工程> --pcc-id <PCC名称>
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_SUPF_5_1_2_4` |
| `--pcc-id` | 已存在的 PCC 规则 ID | **必填** |
| `--headed` | 显示浏览器窗口 | off |
> **链路**:`smpolicy/default/index` → 编辑 `sm_policy_default` 弹窗 → pccRules xm-select 中追加指定 PCC。
**示例**:
```bash
# 将 PCC 添加到 sm_policy_default
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_high_rate
# 添加多个 PCC 规则
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_default
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_video
```
---
#### smpolicy-ue-add-skill.js {#smpolicy-ue-add-skilljs}
**功能**:在指定工程下添加一条 UE Smpolicy 规则,按 IMSI/DNN/sNssai 匹配 UE 并关联 PCC 规则。
**使用方式**:
```bash
node 5gc.js smpolicy ue-add --project <工程> --name <名称> --dnn <DNN> [选项...]
node skills/5gc/scripts/smpolicy-ue-add-skill.js --project <工程> --name <名称> --dnn <DNN> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--name` | UE策略名称(字母/数字/下划线) | **必填** |
| `--dnn` | DNN | **必填** |
| `--imsi` | IMSI 起始值(不填则自动生成) | 自动 |
| `--imsi-num` | IMSI 数量 | `1` |
| `--sst` | sNssai SST | `1` |
| `--sd` | sNssai SD | `111111` |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) | |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) | |
| `--pra-rules` | PRA规则(xm-select,可选) | |
| `--ref-qos-timer` | reflectiveQoSTimer 值(秒) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet
# 指定 IMSI 和 sNssai
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet \
--imsi 460001234567890 --sst 1 --sd 111111
# 绑定 PCC 规则(多个逗号分隔)
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy2 --dnn internet \
--pcc-rules "pcc2,pcc_default"
# 指定反射 QoS 定时器
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy3 --dnn internet \
--pcc-rules pcc2 --ref-qos-timer 60
```
#### smpolicy-ue-edit-skill.js
**功能**:编辑已有 UE Smpolicy 规则的字段(DNN、sNssai、PCC 绑定等)。
**使用方式**:
```bash
node 5gc.js smpolicy ue-edit --project <工程> --name <名称> [--dnn <新DNN>] [--pcc-rules <规则>] [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要编辑的 UE 策略名称(精确匹配) |
| `--dnn` | 新 DNN |
| `--imsi` | 新 IMSI 起始值 |
| `--sst` | 新 sNssai SST |
| `--sd` | 新 sNssai SD |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) |
| `--pra-rules` | PRA规则(xm-select) |
| `--ref-qos-timer` | reflectiveQoSTimer |
| `--headed` | 显示浏览器窗口 |
> ⚠️ xm-select 为多选模式。指定 `--pcc-rules` 时会叠加选中已有规则;编辑时需注意 toggle 行为。
**示例**:
```bash
# 修改 DNN
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet_new
# 修改 PCC 绑定
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --pcc-rules pcc2,pcc_reg_test
# 修改 sNssai
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --sst 1 --sd 222222
```
#### smpolicy-dnn-add-skill.js {#smpolicy-dnn}
**功能**:在指定工程下添加一条 DNN Smpolicy 规则,按 DNN/sNssai 匹配会话并关联 PCC 规则。
**使用方式**:
```bash
node 5gc.js smpolicy dnn-add --project <工程> --name <名称> --dnn <DNN> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--name` | DNN策略名称(必填) | **必填** |
| `--dnn` | DNN值(必填) | **必填** |
| `--sst` | sNssai SST | `1` |
| `--sd` | sNssai SD | `111111` |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) | |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) | |
| `--pra-rules` | PRA规则(xm-select,可选) | |
| `--ref-qos-timer` | reflectiveQoSTimer(秒) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet
# 绑定 PCC 规则
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet --pcc-rules pcc2
# 多个 PCC 规则
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy2 --dnn internet --pcc-rules "pcc2,pcc_default"
```
#### smpolicy-dnn-edit-skill.js
**功能**:编辑已有 DNN Smpolicy 规则的字段(DNN、sNssai、PCC 绑定等)。
**使用方式**:
```bash
node 5gc.js smpolicy dnn-edit --project <工程> --name <名称> [--dnn <新DNN>] [--pcc-rules <规则>] [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要编辑的 DNN 策略名称(精确匹配) |
| `--dnn` | 新 DNN 值 |
| `--sst` | 新 sNssai SST |
| `--sd` | 新 sNssai SD |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) |
| `--pra-rules` | PRA规则(xm-select) |
| `--ref-qos-timer` | reflectiveQoSTimer |
| `--headed` | 显示浏览器窗口 |
> ⚠️ xm-select 为多选模式。指定 `--pcc-rules` 时会叠加选中已有规则;编辑时需注意 toggle 行为。
**示例**:
```bash
# 修改 DNN
node 5gc.js smpolicy dnn-edit --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet_new
# 修改 PCC 绑定
node 5gc.js smpolicy dnn-edit --project XW_SUPF_5_1_2_4 --name dnn_policy1 --pcc-rules pcc2,pcc_default
```
---
## 全局参数参考
以下参数所有脚本均支持:
| 参数 | 说明 | 适用范围 |
|------|------|---------|
| `--url <地址>` | 5GC 仪表 URL | 所有脚本 |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | 所有脚本 |
| `--headed` | 打开可见 Chromium 窗口(调试用) | 所有脚本 |
| `--set-<字段> <值>` | 修改指定字段值 | 所有 edit 脚本 |
| `--name <名称>` | 按名称精确匹配 | 所有 edit 脚本 |
| `--id <ID>` | 按 ID 直接定位 | 所有 edit 脚本 |
---
## 字段参考
### AMF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `mcc` | 移动国家码 | `460` |
| `mnc` | 移动网络码 | `01` |
| `ngap_sip` | NGAP 信令面 IP | `10.200.1.50` |
| `ngap_port` | NGAP 端口 | `38412` |
| `http2_sip` | HTTP2 服务 IP | `10.200.1.51` |
| `http2_port` | HTTP2 端口 | `8080` |
| `stac` | 起始 TAC | `101` |
| `etac` | 结束 TAC | `102` |
| `region_id` | 区域 ID | `1` |
| `set_id` | Set ID | `1` |
| `pointer` | 指针 | `1` |
| `ea[NEA0]` ~ `ea[128-NEA3]` | 加密算法(默认全选) | `1` |
| `ia[NIA0]` ~ `ia[128-NIA3]` | 完整性保护算法(默认全选) | `1` |
### UDM/AUSF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `count` | 实例数量 | `3` |
| `sip` | SIP 服务 IP | `10.0.0.100` |
| `port` | 端口 | `80` |
| `auth_method` | 认证方法 | `5G_AKA` |
| `scheme` | 协议类型 | `HTTP` |
| `priority` | 优先级 | `8` |
### SMF/PGW-C 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `pfcp_sip` | PFCP 信令面 IP | `10.10.10.50` |
| `n3_ip` | N3 接口 IP | `10.10.10.50` |
| `n6_ip` | N6 接口 IP | `10.10.10.51` |
| `http2_sip` | HTTP2 服务 IP | `10.10.10.50` |
| `dnn` | DNN(数据网络名) | `internet` |
| `snssai_sst` | NSSAI SST | `1` |
| `snssai_sd` | NSSAI SD | `ffffff` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `01` |
| `pdu_capacity` | PDU 会话容量 | `200000` |
### UPF/PGW-U 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `n3_ip` | N3 接口 IP | `192.168.20.30` |
| `n4_ip` | N4 接口 IP(PFCP) | `192.168.20.30` |
| `n6_ip` | N6 接口 IP | `192.168.20.31` |
| `n6_gw` | N6 网关 IP | `192.168.20.1` |
| `dnn` | DNN | `internet` |
| `static_arp` | 静态 ARP | `192.168.20.254` |
| `sst` | NSSAI SST | `1` |
| `sd` | NSSAI SD | `ffffff` |
| `stac` | 起始 TAC | `101` |
| `etac` | 结束 TAC | `102` |
### GNB 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `ngap_sip` | NGAP 信令面 IP | `200.20.20.50` |
| `user_sip_ip_v4` | 用户面 IPv4 | `2.2.2.2` |
| `user_sip_ip_v6` | 用户面 IPv6 | `::1` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `60` |
| `stac` | 起始 TAC | `0` |
| `etac` | 结束 TAC | `0` |
| `node_id` | 节点 ID | `70` |
| `cell_count` | 小区数量 | `1` |
| `replay_ip` | 回放 IP | `0.0.0.0` |
| `replay_port` | 回放端口 | `0` |
### UE 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `s_imsi` | 起始 IMSI(15位) | `460001234567890` |
| `msisdn` | MSISDN(13-15位,86开头) | `8613888888888` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `01` |
| `key` | KI 密钥(32位 hex) | `001122...` |
| `op_opc` | OPc 密钥(32位 hex) | `aabbcc...` |
| `imeisv` | IMEISV(15位,偶数) | `8611111111111111` |
| `nssai_sst` | NSSAI SST | `1` |
| `nssai_sd` | NSSAI SD | `111111` |
| `user_sip_ip_v4` | 用户面 IPv4 | `自动分配` |
| `user_sip_ip_v6` | 用户面 IPv6 | `自动分配` |
| `replay_ip` | 回放 IP | `0.0.0.0` |
| `replay_port` | 回放端口 | `0` |
#### default-rule-add-skill.js(PCF 默认规则一键配置)
**功能**:为指定工程一键配置完整的 PCF 默认规则链路,包含 QoS 模板 → Traffic Control → PCC 规则 → sm_policy_default → PCF default_smpolicy 全五步。
**使用方式**:
```bash
node 5gc.js pcf default-rule-add --project <工程> [选项...]
node skills/5gc/scripts/default-rule-add-skill.js --project <工程> [选项...]
```
**参数**(全部可选,有默认值):
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--pcf-name` | **PCF 实例名称**(必填,指定要为哪个 PCF 配置默认规则) | 无 |
| `--qos-id` | QoS 模板 ID | `qos_default_{时间戳}` |
| `--5qi` | 5QI 值(不指定则自动选择未使用的值) | 自动(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率 | `10000000` |
| `--maxbr-dl` | 下行最大比特率 | `20000000` |
| `--gbr-ul` | 上行保证比特率 | `5000000` |
| `--gbr-dl` | 下行保证比特率 | `5000000` |
| `--tc-id` | TC 规则 ID | `tc_default_{时间戳}` |
| `--flow-status` | TC 流状态 | `ENABLED` |
| `--pcc-id` | PCC 规则 ID | `pcc_default` |
| `--precedence` | PCC 优先级 | `63` |
| `--headed` | 显示浏览器窗口(调试用) | off |
**示例**:
```bash
# 最简用法(自动生成所有 ID)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
# 指定 QoS 参数(高速率)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc \
--qos-id qos_high_rate --5qi 8 \
--maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 指定 PCC 优先级
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --pcc-id pcc_new --precedence 50
# 调试模式
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --headed
```
**完整链路**:
1. ✅ **QoS 模板创建**:自动选择未使用的 5QI,创建 QoS 模板
2. ✅ **Traffic Control 创建**:创建 ENABLED 状态的 TC 规则
3. ✅ **PCC 规则创建**:创建 PCC 规则,绑定 QoS 和 TC
4. ✅ **sm_policy_default 创建/更新**:创建或更新默认会话策略,绑定 PCC 规则
5. ✅ **PCF default_smpolicy 设置**:为指定 PCF 实例设置 default_smpolicy 为 sm_policy_default
**注意事项**:
- 同一工程多次运行会自动删除旧的同名资源并重建,不会污染配置
- 必须指定 `--pcf-name` 参数,明确要为哪个 PCF 实例配置默认规则
- 脚本会自动处理弹窗(iframe)和 CSRF token,无需手动操作
- 所有步骤都有验证检查,确保配置成功
**已测试工程**:
- ✅ XW_SUPF_5_1_11_2(PCF "qqq")
- ✅ XW_SUPF_5_1_8_1(PCF "pcc")
- ✅ XW_SUPF_5_1_4_1(PCF "pcc")
### PCF/PCRF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `http2_sip` | HTTP2 服务 IP | `192.168.20.90` |
| `http2_port` | HTTP2 端口 | `80` |
| `MCC` | MCC(大写) | `460` |
| `MNC` | MNC(大写) | `01` |
| `count` | 实例数量 | `1` |
FILE:scripts/5gc.js
/**
* 5GC Web 仪表统一 CLI
*
* 用法: node 5gc.js <entity> <action> [options]
*
* entity (网元类型): amf | udm | smf | upf | gnb | ue | pcf | nrf | qos | tc | smpolicy
* action (操作类型): add | edit | default-rule-add | default-rule-edit
*
* 通用选项:
* --url <地址> 5GC 仪表地址(默认 https://192.168.3.89)
* --project <工程> 目标工程名称
* --name <名称> 网元名称(用于单条记录筛选)
* --id <id> 网元 ID(直接编辑指定 ID)
* --headed 以有头模式运行(显示浏览器窗口)
*
* 字段修改(edit 模式)--set-<field> <value>:
* AMF: name|sbi_ip|sbi_port|amf_name|guami|mcc|mnc|sst|sd|ap1|ap2|ap3|ap4|ap5
* UDM: name|auth_supi|auth_op_type|op_opc|aud_method|scheme|id|priority
* SMF: name|pfcp_ip|n3_ip|n6_ip|dnn|snssai|sliceamba_type
* UPF: name|n4_ip|n3_ip|n6_ip|dnn|snssai|count|static_arp|ue_ip_pool
* GNB: name|ngap_ip|user_sip_ip_v4|mcc|mnc|stac|etac|node_id|cell_count|replay_ip|replay_port
* UE: name|count|mcc|mnc|s_imsi|key|opc|imeisv|msisdn|user_sip_ip_v4|user_sip_ip_v6|replay_ip|replay_port
*
* 示例:
* node 5gc.js amf add --name AMF_TEST --project XW_S5GC_1 --sbi-ip 10.0.0.1
* node 5gc.js gnb add --name GNB_TEST --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100
* node 5gc.js ue add --name UE_001 --imsi 460001234567890 --msisdn 8613888888888
* node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613888888888
* node 5gc.js ue edit --id 10337 --set-msisdn 8613888888888
* node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 200.200.200.200
* node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 10.0.0.5
* node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
* node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --precedence 50
* node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_new --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000
* node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED
* node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
* node 5gc.js smpolicy default-add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_new
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const SCRIPTS_DIR = __dirname;
const argv = process.argv.slice(2);
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
printHelp();
process.exit(0);
}
const entity = argv[0].toLowerCase();
const action = (argv[1] || '').toLowerCase();
const VALID_ENTITIES = ['amf', 'udm', 'smf', 'upf', 'gnb', 'ue', 'pcf', 'pcc', 'nrf', 'qos', 'tc', 'smpolicy'];
const VALID_ACTIONS = ['add', 'edit', 'default-rule-add', 'add-pcc', 'ue-add', 'ue-edit', 'dnn-add', 'dnn-edit'];
if (!VALID_ENTITIES.includes(entity)) {
console.error(`\n❌ 未知网元类型: entity`);
console.error(' 可用: ' + VALID_ENTITIES.join(', '));
process.exit(1);
}
if (!action || !VALID_ACTIONS.includes(action)) {
console.error(`\n❌ 未知操作: action || '(空)'`);
console.error(' 用法: node 5gc.js <entity> <action> [options]');
console.error(' 示例: node 5gc.js amf add --help');
process.exit(1);
}
// 子脚本映射
// 所有 edit 均映射到 edit 脚本(单条 + 批量二合一)
const scriptMap = {
'amf:add': 'amf-add-skill.js',
'amf:edit': 'amf-edit-skill.js',
'udm:add': 'ausf-udm-add-skill.js',
'udm:edit': 'ausf-udm-edit-skill.js',
'smf:add': 'smf-pgwc-add-skill.js',
'smf:edit': 'smf-pgwc-edit-skill.js',
'upf:add': 'upf-add-skill.js',
'upf:edit': 'upf-edit-skill.js',
'gnb:add': 'gnb-add-skill.js',
'gnb:edit': 'gnb-edit-skill.js',
'ue:add': 'ue-add-skill.js',
'ue:edit': 'ue-edit-skill.js',
'pcf:add': 'pcf-add-skill.js',
'pcf:edit': 'pcf-edit-skill.js',
'pcf:default-rule-add': 'default-rule-add-skill.js',
'pcc:add': 'pcc-add-skill.js',
'pcc:edit': 'pcc-edit-skill.js',
'nrf:add': 'nrf-add-skill.js',
'nrf:edit': 'nrf-edit-skill.js',
'qos:add': 'qos-add-skill.js',
'tc:add': 'tc-add-skill.js',
'smpolicy:add-pcc': 'smpolicy_add_pcc.js',
'smpolicy:ue-add': 'smpolicy-ue-add-skill.js',
'smpolicy:ue-edit': 'smpolicy-ue-edit-skill.js',
'smpolicy:dnn-add': 'smpolicy-dnn-add-skill.js',
'smpolicy:dnn-edit': 'smpolicy-dnn-edit-skill.js',
};
const scriptFile = scriptMap[`entity:action`];
const scriptPath = path.join(SCRIPTS_DIR, scriptFile);
if (!fs.existsSync(scriptPath)) {
console.error(`\n❌ 脚本不存在: scriptPath`);
process.exit(1);
}
function normalizeChildArgs(entity, action, args) {
const out = [];
let positionalName = null;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const next = i + 1 < args.length ? args[i + 1] : undefined;
if (arg === '--name' && next !== undefined) {
if (entity === 'ue' && action === 'add') {
out.push('--name', next);
} else {
positionalName = next;
}
i++;
continue;
}
if ((entity === 'smf' || entity === 'upf' || entity === 'gnb') && arg === '--pfcp-ip' && next !== undefined) {
out.push('--pfcp_sip', next); i++; continue;
}
if (entity === 'smf' && arg === '--n3-ip' && next !== undefined) {
out.push('--http2_sip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n4-ip' && next !== undefined) {
out.push('--n4_ip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n3-ip' && next !== undefined) {
out.push('--n3_ip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n6-ip' && next !== undefined) {
out.push('--n6_ip', next); i++; continue;
}
if (entity === 'gnb' && arg === '--ngap-ip' && next !== undefined) {
out.push('--ngap_sip', next); i++; continue;
}
if (entity === 'gnb' && arg === '--user-sip-ip-v4' && next !== undefined) {
out.push('--user_sip_ip_v4', next); i++; continue;
}
if (entity === 'gnb' && arg === '--node-id' && next !== undefined) {
out.push('--node_id', next); i++; continue;
}
if (entity === 'amf' && action === 'add') {
if (arg === '--sbi-ip' && next !== undefined) { out.push('--http2_sip', next); i++; continue; }
if (arg === '--sst' && next !== undefined) { i++; continue; }
if (arg === '--sd' && next !== undefined) { i++; continue; }
}
if (entity === 'udm' && action === 'add') {
if (arg === '--auth-supi' && next !== undefined) { i++; continue; }
if (arg === '--auth-op-type' && next !== undefined) { i++; continue; }
if (arg === '--opc' && next !== undefined) { out.push('--op_opc', next); i++; continue; }
}
out.push(arg);
}
if (positionalName) out.unshift(positionalName);
return out;
}
// 去掉 entity 和 action 后的参数传给子脚本
const childArgv = normalizeChildArgs(entity, action, argv.slice(2));
console.log(`\n▶ 5GC entity.toUpperCase() action`);
console.log(' → node ' + scriptFile + ' ' + childArgv.join(' ') + '\n');
// 用子进程调用,保持 CLI 参数隔离
const child = spawn('node', [scriptPath, ...childArgv], {
stdio: 'inherit',
shell: true,
cwd: SCRIPTS_DIR,
});
child.on('exit', (code) => process.exit(code || 0));
child.on('error', (err) => { console.error('启动失败:', err.message); process.exit(1); });
function printHelp() {
console.log(`
5GC Web 仪表自动化 - 统一 CLI
=============================
用法:
node 5gc.js <entity> <action> [options]
网元类型 (entity):
amf - AMF(接入与移动性管理功能)
udm - UDM/AUSF(统一数据管理/认证服务器功能)
smf - SMF/PGW-C(会话管理功能/PDN 连接网关控制面)
upf - UPF/PGW-U(用户面功能/PDN 连接网关用户面)
gnb - gNodeB(5G 基站)
ue - UE(用户终端)
pcf - PCF/PCRF(策略控制功能)
nrf - NRF(网络存储功能)
qos - QoS 模板
tc - Traffic Control 流量控制规则
smpolicy - Smpolicy(会话策略规则)
操作 (action):
add - 添加网元实例
edit - 编辑网元(单个或批量)
default-rule-add - 一键配置完整 PCF 默认规则链路(QoS → TC → PCC → sm_policy_default → PCF)
通用选项:
--url <地址> 5GC 仪表地址(默认 https://192.168.3.89)
--project <工程> 目标工程名称
--name <名称> 网元名称
--id <id> 网元 ID(edit 模式)
--headed 以有头模式运行(显示浏览器)
--help 显示本帮助
字段修改(edit 模式 --set-<field> <value>):
AMF: name|sbi_ip|sbi_port|amf_name|guami|mcc|mnc|sst|sd|ap1|ap2|ap3|ap4|ap5
UDM: name|auth_supi|auth_op_type|op_opc|aud_method|scheme|id|priority
SMF: name|pfcp_ip|n3_ip|n6_ip|dnn|snssai|sliceamba_type
UPF: name|n4_ip|n3_ip|n6_ip|dnn|snssai|count|static_arp|ue_ip_pool
GNB: name|ngap_ip|user_sip_ip_v4|mcc|mnc|stac|etac|node_id|cell_count|replay_ip|replay_port
UE: name|count|mcc|mnc|s_imsi|key|opc|imeisv|msisdn|user_sip_ip_v4|user_sip_ip_v6|replay_ip|replay_port
PCF: http2_sip|http2_port|mcc|mnc
PCF默认规则: --pcf-name <名称> --qos-id <ID> --tc-id <ID> --pcc-id <ID> --precedence <值>
添加示例:
node 5gc.js amf add --name AMF_TEST --project XW_S5GC_1 --sbi-ip 10.0.0.1 --mcc 460 --mnc 01
node 5gc.js gnb add --name GNB_TEST --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100
node 5gc.js ue add --name UE_001 --imsi 460001234567890 --msisdn 8613888888888
node 5gc.js smf add --name SMF_TEST --project XW_S5GC_1 --pfcp-ip 10.0.0.2
node 5gc.js upf add --name UPF_TEST --project XW_S5GC_1 --n4-ip 10.0.0.3
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_new --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --precedence 50
编辑示例:
node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613888888888
node 5gc.js ue edit --id 10337 --set-msisdn 8613888888888
node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 200.200.200.200
node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 10.0.0.5
`);
}
FILE:scripts/amf-add-skill.js
#!/usr/bin/env node
/**
* AMF 添加脚本 - 完整修复版
* 功能:登录状态缓存 + .projectSelect 选工程 + evaluate 填写表单 + 算法全勾选 + NSSAI
*/
const { chromium } = require('playwright');
let globalBaseUrl = 'https://192.168.3.89';
const fs = require('fs');
const path = require('path');
// 配置
const CONFIG = {
urls: {
login: '/login',
amfEdit: '/sim_5gc/amf/edit',
amfManagement: '/sim_5gc/amf/index'
},
credentials: {
email: '[email protected]',
password: 'dotouch'
},
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
const host = globalBaseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
};
// 会话管理
class SessionManager {
constructor() {
this.sessionPath = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async saveSession(context) {
try {
const storageState = await context.storageState();
fs.writeFileSync(this.sessionPath, JSON.stringify({ storageState }, null, 2));
return true;
} catch {
return false;
}
}
async loadSession(browser) {
try {
if (!fs.existsSync(this.sessionPath)) return null;
const { storageState } = JSON.parse(fs.readFileSync(this.sessionPath, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
} catch {
return null;
}
}
}
// 算法配置:直接点击 layui 复选框的可见元素
async function configureAlgorithmsSuccess(page) {
await page.waitForSelector('.layui-form-checkbox', { timeout: 5000 });
await page.waitForTimeout(300);
const checkboxCount = await page.locator('.layui-form-checkbox').count();
console.log(` 算法复选框数量: checkboxCount`);
for (let i = 0; i < Math.min(checkboxCount, 8); i++) {
await page.locator('.layui-form-checkbox').nth(i).click();
await page.waitForTimeout(80);
}
const priorities = [
'ea[NEA0]', 'ea[128-NEA1]', 'ea[128-NEA2]', 'ea[128-NEA3]',
'ia[NIA0]', 'ia[128-NIA1]', 'ia[128-NIA2]', 'ia[128-NIA3]'
];
const vals = ['1', '2', '3', '4', '1', '2', '3', '4'];
for (let i = 0; i < priorities.length; i++) {
const inp = page.locator(`input[name="priorities[i]"]`);
if (await inp.count() > 0) {
await inp.fill(vals[i]);
}
}
console.log(` ✅ 算法配置完成`);
}
// 工程选择(精确匹配,分页遍历)
async function selectProject(page, projectName, forceSwitch = true) {
if (!forceSwitch) {
console.log(` 🔧 保持当前工程(用户未指定工程)`);
return true;
}
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.evaluate(() => {
const inputs = document.querySelectorAll('input[type="text"], input[name="name"]');
for (const inp of inputs) { inp.value = ''; }
});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) {
icon.click();
return true;
}
}
}
return false;
}, projectName);
if (clicked) {
await page.waitForTimeout(2000);
return true;
}
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try {
REPLACED
} catch (e) {
break;
}
}
console.log(` ❌ 未找到工程 "projectName"(精确匹配)`);
return false;
}
// 添加 AMF 主流程
async function addAmf(amfName, projectName, explicitProject = true, amfConfig = {}) {
const startTime = Date.now();
const sessionManager = new SessionManager();
const defaultConfig = {
mcc: '460', mnc: '01', region_id: '1', set_id: '1', pointer: '1',
ngap_sip: '200.20.20.1', ngap_port: '38412',
http2_sip: '200.20.20.5', http2_port: '8080',
stac: '101', etac: '102'
};
const cfg = { ...defaultConfig, ...amfConfig };
let browser = null;
try {
browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
let context = await sessionManager.loadSession(browser);
let needLogin = true;
if (context) {
const testPage = await context.newPage();
await testPage.goto(`globalBaseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
if (!testPage.url().includes('/login')) {
needLogin = false;
}
await testPage.close();
}
if (needLogin) {
context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
}
const page = await context.newPage();
if (needLogin) {
await page.goto(`globalBaseUrlCONFIG.urls.login`, { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle', { timeout: 10000 });
await sessionManager.saveSession(context);
}
// 选择工程(仅当用户显式指定工程时才切换)
if (!(await selectProject(page, projectName, explicitProject))) {
throw new Error(`工程 "projectName" 不存在或无法选中`);
}
// 进入编辑页面
await page.goto(`globalBaseUrlCONFIG.urls.amfEdit`, { waitUntil: 'networkidle', timeout: 15000 });
if (!page.url().includes('/amf/edit')) {
await page.goto(`globalBaseUrl/sim_5gc/amf/edit`);
await page.waitForSelector('input[name="name"]', { timeout: 10000 });
}
// 通过 evaluate 直接填写表单
await page.evaluate(({ amfName, cfg }) => {
const set = (name, value) => {
const el = document.querySelector(`input[name="name"]`);
if (el) {
el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
}
};
set('name', amfName);
set('mcc', cfg.mcc);
set('mnc', cfg.mnc);
set('region_id', cfg.region_id);
set('set_id', cfg.set_id);
set('pointer', cfg.pointer);
set('ngap_sip', cfg.ngap_sip);
set('ngap_port', cfg.ngap_port);
set('http2_sip', cfg.http2_sip);
set('http2_port', cfg.http2_port);
set('stac', cfg.stac);
set('etac', cfg.etac);
}, { amfName, cfg });
// 类型选择:仿真设备
await page.locator('.layui-unselect').first().click();
await page.waitForTimeout(300);
await page.locator('dd').filter({ hasText: '仿真设备' }).click();
// 配置算法
await configureAlgorithmsSuccess(page);
// 配置 NSSAI
await page.getByRole('row', { name: /数量.*nssai/ }).getByRole('button').click();
await page.waitForTimeout(500);
await page.locator('input[name="config[count][]"]').fill('1');
await page.getByRole('row', { name: /nssai.*添加.*删除/ }).locator('span').click();
await page.waitForTimeout(800);
const iframeEl = page.locator('iframe[name="layui-layer-iframe2"]');
const iframe = await iframeEl.contentFrame({ timeout: 5000 });
await iframe.getByRole('row', { name: /\*.*SST.*SD/ }).getByRole('button').click();
await iframe.locator('input[name="nssai[snssai_sst][]"]').fill('1');
await iframe.locator('input[name="nssai[snssai_sd][]"]').fill('111111');
await iframe.getByRole('button', { name: '提交' }).click();
await page.waitForTimeout(800);
// 提交表单
await page.getByRole('button', { name: '提交' }).click();
// 等待页面跳转到 AMF 列表页面,若未跳转则强制跳转
try {
await page.waitForURL(`**/amf/index`, { timeout: 8000 });
} catch (e) {
await page.goto(`globalBaseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 15000 });
}
await page.waitForTimeout(2000);
// 验证结果:只要页面成功跳转到 AMF 列表页,即认为添加成功
let found = false;
const finalUrl = page.url();
if (finalUrl.includes('/amf/index')) {
console.log(` ✅ 页面已跳转至 AMF 列表: finalUrl`);
found = true;
}
await browser.close();
const totalTime = (Date.now() - startTime) / 1000;
if (found) {
return { success: true, amfName, totalTime };
} else {
return { success: false, amfName, totalTime };
}
} catch (err) {
if (browser) await browser.close();
throw err;
}
}
// 主函数
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('用法: node amf-add-skill.js <AMF名称> [--project <工程名>] [--url <地址>] [--mcc 460] [...]');
process.exit(1);
}
let amfName = null;
let projectName = '5G_basic_process';
let amfConfig = {};
let explicitProject = false;
for (let i = 0; i < args.length; i++) {
if (!args[i].startsWith('-')) {
amfName = args[i];
} else if (args[i] === '--project' || args[i] === '-p') {
projectName = args[++i];
explicitProject = true;
} else if (args[i] === '--url') {
let u = args[++i];
if (u && !u.startsWith('http')) u = 'https://' + u;
globalBaseUrl = u;
} else if (args[i].startsWith('--')) {
amfConfig[args[i].substring(2)] = args[++i];
}
}
if (!amfName) {
console.error('错误: 请指定 AMF 名称');
process.exit(1);
}
console.log(`AMF: amfName | 工程: projectName | 地址: globalBaseUrl`);
try {
const result = await addAmf(amfName, projectName, explicitProject, amfConfig);
console.log(result.success
? `成功! AMF "result.amfName" 添加完成 (result.totalTime.toFixed(2)s)`
: `失败! 未找到 AMF "result.amfName"`);
process.exit(result.success ? 0 : 1);
} catch (err) {
console.error(`执行异常: err.message`);
process.exit(1);
}
}
main();
FILE:scripts/default-rule-add-skill.js
/**
* default-rule-add-skill.js - PCF 默认规则一键添加工具
*
* 完整链路(一次性完成):
* 1. 创建 QoS 模板(自动选5qi)
* 2. 创建 Traffic Control(ENABLED)
* 3. 创建 PCC 规则(绑定 qos + tc)
* 4. 创建/更新 sm_policy_default(绑定 pcc)
* 5. PCF default_smpolicy → sm_policy_default
*
* 用法:
* node default-rule-add-skill.js --project XW_SUPF_5_1_2_4 --headed
* node default-rule-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --headed
*
* 参数(均有默认值,可全部省略):
* --project 工程名(默认 XW_S5GC_1)
* --pcf-name PCF实例名称(必填,如 qqq)
* --qos-id QoS模板ID(默认自动生成 qos_default_{timestamp})
* --5qi 5QI值(不指定则自动选择未使用的值)
* --maxbr-ul 上行最大比特率(默认 10000000)
* --maxbr-dl 下行最大比特率(默认 20000000)
* --gbr-ul 上行保证比特率(默认 5000000)
* --gbr-dl 下行保证比特率(默认 5000000)
* --tc-id TC规则ID(默认自动生成 tc_default_{timestamp})
* --flow-status TC流状态(默认 ENABLED)
* --pcc-id PCC规则ID(默认 pcc_default)
* --precedence PCC优先级(默认 63)
* --headed 显示浏览器窗口
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const ts = Date.now();
const opts = {
project: 'XW_S5GC_1',
pcfName: null, // null = 使用 pccId 作为 PCF 名称(向后兼容)
// QoS 参数
qosId: null, // null = 自动生成
qi: null,
maxbrUl: '10000000',
maxbrDl: '20000000',
gbrUl: '5000000',
gbrDl: '5000000',
// TC 参数
tcId: null, // null = 自动生成
flowStatus: 'ENABLED',
// PCC 参数
pccId: null, // null = 自动生成
precedence: '63',
// PCF 参数(网元名称)
pcfName: null, // 若未提供则使用 pccId 作为默认名称
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--qos-id') opts.qosId = args[++i];
else if (args[i] === '--5qi') opts.qi = args[++i];
else if (args[i] === '--maxbr-ul') opts.maxbrUl = args[++i];
else if (args[i] === '--maxbr-dl') opts.maxbrDl = args[++i];
else if (args[i] === '--gbr-ul') opts.gbrUl = args[++i];
else if (args[i] === '--gbr-dl') opts.gbrDl = args[++i];
else if (args[i] === '--tc-id') opts.tcId = args[++i];
else if (args[i] === '--flow-status') opts.flowStatus = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--precedence') opts.precedence = args[++i];
else if (args[i] === '--pcf-name') opts.pcfName = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
// 自动生成ID(如果未指定)
if (!opts.qosId) opts.qosId = `qos_default_ts`;
if (!opts.tcId) opts.tcId = `tc_default_ts`;
if (!opts.pccId) opts.pccId = `pcc_default`;
// 如果未提供 PCF 名称,默认使用 PCC ID(与业务保持一致)
if (!opts.pcfName) opts.pcfName = opts.pccId;
return opts;
}
// ─── 通用工具 ────────────────────────────────────────────────────────────
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let clicked = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
clicked = true;
}
});
return clicked;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function goto(page, url) {
await page.goto(`globalBaseUrlurl`, { waitUntil: 'load', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
}
// ─── Step 1: 创建 QoS 模板 ──────────────────────────────────────────────
async function getUsedQis(page) {
await goto(page, '/sim_5gc/predfPolicy/qos/index');
return await page.evaluate(() => {
const qis = [];
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[3].textContent.trim()) {
qis.push(parseInt(cells[3].textContent.trim()));
}
});
return qis;
});
}
async function addQos(page, opts) {
// 自动选 5qi(先获取已用列表)
if (!opts.qi) {
await goto(page, '/sim_5gc/predfPolicy/qos/index');
const used = await page.evaluate(() => {
const qis = [];
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[3].textContent.trim()) {
qis.push(parseInt(cells[3].textContent.trim()));
}
});
return qis;
});
const candidates = [8, 9, 6, 5, 7, 4, 3, 2, 1];
for (const c of candidates) { if (!used.includes(c)) { opts.qi = String(c); break; } }
if (!opts.qi) opts.qi = String(used[0] + 1);
console.log(` i️ 已用5qi: used.join(','),自动选择 opts.qi`);
}
await goto(page, '/sim_5gc/predfPolicy/qos/index');
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 10000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(500);
await frame.locator('input[name="qosId"]').fill(opts.qosId);
await frame.locator('input[name="5qi"]').fill(opts.qi);
await frame.locator('input[name="maxbrUl"]').fill(opts.maxbrUl);
await frame.locator('input[name="maxbrDl"]').fill(opts.maxbrDl);
await frame.locator('input[name="gbrUl"]').fill(opts.gbrUl);
await frame.locator('input[name="gbrDl"]').fill(opts.gbrDl);
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ QoS模板 opts.qosId 已创建 (5qi=opts.qi)`);
}
// ─── Step 2: 创建 TC ────────────────────────────────────────────────────
async function addTc(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/trafficCtl/index');
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 等待 iframe 出现在 DOM 中
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 10000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
// 等待 tcId input 出现
await frame.waitForSelector('input[name="tcId"]', { timeout: 10000 });
await page.waitForTimeout(500);
await frame.locator('input[name="tcId"]').fill(opts.tcId);
// 等待 select[name="flowStatus"] 出现在 DOM 中
const sel = frame.locator('select[name="flowStatus"]');
try {
await sel.waitFor({ state: 'attached', timeout: 5000 });
await sel.selectOption(opts.flowStatus, { force: true });
console.log(` flowStatus = opts.flowStatus`);
} catch(e) {
// 如果 select 不存在(如没有 flowStatus 字段),跳过
console.log(` i️ flowStatus select 不存在,跳过`);
}
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ TC opts.tcId 已创建 (flowStatus=opts.flowStatus)`);
}
// ─── Step 3: 创建 PCC ────────────────────────────────────────────────────
async function addPcc(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/pcc/index');
// 检查是否已存在同名 PCC,存在则先删除
const existingId = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[2].textContent.trim() === targetId) {
return cells[1].textContent.trim(); // 返回数字ID
}
}
return null;
}, opts.pccId);
if (existingId) {
// 删除旧记录
await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[1].textContent.trim() === id) {
const links = cells[9].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '删除') { l.click(); return; } }
}
}
}, existingId);
await page.waitForTimeout(1500);
// 处理删除确认对话框
const confirmBtn = page.locator('.layui-layer-dialog .layui-layer-btn0, .layui-layer-btn a:first-child');
if (await confirmBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await confirmBtn.click();
await page.waitForTimeout(2000);
}
// 确保遮罩关闭
await page.keyboard.press('Escape');
await page.waitForTimeout(1000);
console.log(` 🗑️ 已删除旧 PCC opts.pccId,准备重建`);
}
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
await page.locator('input[name="pccRuleId"]').fill(opts.pccId);
await page.locator('input[name="precedence"]').fill(opts.precedence);
// xm-select[0] = refQosData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[0].parentElement.click());
await page.waitForTimeout(1000);
const qosOpt = page.locator('.xm-option.show-icon', { hasText: opts.qosId });
if (await qosOpt.isVisible({ timeout: 3000 }).catch(() => false)) await qosOpt.click();
await page.waitForTimeout(500);
// xm-select[1] = refTcData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[1].parentElement.click());
await page.waitForTimeout(1000);
const tcOpt = page.locator('.xm-option.show-icon', { hasText: opts.tcId });
if (await tcOpt.isVisible({ timeout: 3000 }).catch(() => false)) await tcOpt.click();
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ PCC规则 opts.pccId 已创建 (refQosData=opts.qosId, refTcData=opts.tcId)`);
}
// ─── Step 4: 创建/更新 sm_policy_default(使用正确的 form_data 格式)────────────
async function addOrUpdateSmpolicy(page, pccId) {
console.log(`\n=== Step 4: 创建/更新 sm_policy_default (pccRules=pccId) ===`);
// 1. 进入 PCF,选中 pccId 行,点击 smpolicy 按钮
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const cb = row.querySelector('input[type="checkbox"]');
if (cb) cb.click();
}
}
}, pccId);
await page.waitForTimeout(500);
await page.locator('button:has-text("smpolicy")').click({ force: true });
await page.waitForTimeout(3000);
console.log(' smpolicy 页面 URL:', page.url());
// 2. 获取 CSRF token(从页面)
const token = await page.evaluate(() => document.querySelector('input[name="_token"]')?.value || '');
if (!token) {
console.error(' ❌ 未找到 _token');
return false;
}
console.log(' _token: ...' + token.substring(0, 10) + '...');
// 3. 检查是否已有 sm_policy_default
const existing = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
return { id: cells[1].textContent.trim(), pccRules: cells[4].textContent.trim() };
}
}
return null;
});
if (!existing) {
console.log(' ℹ️ sm_policy_default 不存在,正在创建...');
await page.locator('button:has-text("添加")').click({ force: true });
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 构造正确的 form_data JSON
const formDataJson = JSON.stringify({
name: 'sm_policy_default',
pccRules: [pccId],
reflectiveQoSTimer: 86400
});
const params = new URLSearchParams();
params.append('_token', token);
params.append('form_data', formDataJson);
console.log(' form_data:', formDataJson);
const resp = await frm.evaluate(async (args) => {
const { tok, bodyStr } = args;
try {
const r = await fetch('/sim_5gc/smpolicy/default/edit', {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': tok,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { tok: token, bodyStr: params.toString() });
console.log(' 创建响应:', resp.status);
if (resp.status >= 400) {
try { console.log(' 错误:', JSON.stringify(JSON.parse(resp.body))); }
catch { console.log(' 响应:', resp.body?.substring(0, 200)); }
return false;
} else {
console.log(' ✅ 创建成功!响应:', resp.body?.substring(0, 200));
return true;
}
} else {
console.log(' ✅ sm_policy_default 已存在 (id=' + existing.id + ', pccRules=' + existing.pccRules + ')');
if (existing.pccRules.includes(pccId)) {
console.log(' ✅ pccRules 已包含 ' + pccId);
return true;
} else {
console.log(' ℹ️ 更新 sm_policy_default,添加 pccRules=' + pccId + '...');
// 点击编辑
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
}
});
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 获取当前 pccRules
const currentPcc = await frm.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs.length > 1 ? inputs[1].value : '';
});
const existingRules = currentPcc ? currentPcc.split(',').filter(Boolean) : [];
if (!existingRules.includes(pccId)) existingRules.push(pccId);
const recId = await frm.evaluate(() => {
const el = document.querySelector('input[name="id"]');
return el ? el.value : '';
});
// 更新用的 form_data
const formDataJson = JSON.stringify({
name: 'sm_policy_default',
pccRules: existingRules,
reflectiveQoSTimer: 86400,
id: recId
});
const params = new URLSearchParams();
params.append('_token', token);
params.append('form_data', formDataJson);
const resp = await frm.evaluate(async (args) => {
const { tok, bodyStr, recId } = args;
try {
const r = await fetch('/sim_5gc/smpolicy/default/edit/' + recId, {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': tok,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { tok: token, bodyStr: params.toString(), recId });
console.log(' 更新响应:', resp.status);
if (resp.status >= 400) {
try { console.log(' 错误:', JSON.stringify(JSON.parse(resp.body))); }
catch { console.log(' 响应:', resp.body?.substring(0, 200)); }
return false;
} else {
console.log(' ✅ 更新成功!响应:', resp.body?.substring(0, 200));
return true;
}
}
}
}// ─── Step 5: PCF default_smpolicy ────────────────────────────────────────
// 正确流程(根据 UI 调试结果):
// 1. 在 PCF 列表先选中 qqq 行(单击,不要点编辑)
// 2. 再点击工具栏 "smpolicy" 按钮 → 页面加载 sm_policy_default 表单(带 qqq 上下文)
// 3. 创建 sm_policy_default(此时 name 应为 qqq 关联的默认策略名)
// 4. 保存后返回 PCF 编辑弹窗 → default_smpolicy 下拉有数据 → 选择 → 提交
async function setPcfDefaultSmpolicy(page, pcfName) {
console.log(`\n=== Step 5: 配置 PCF "pcfName" default_smpolicy ===`);
// 1. 进入 PCF 列表,点击指定 PCF 的编辑按钮
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return true; } }
}
}
return false;
}, pcfName);
if (!clicked) {
console.error(` ❌ 未找到 PCF "pcfName" 的编辑按钮`);
return false;
}
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 2. 获取 token 和 PCF ID
const token = await frm.evaluate(() => {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
});
if (!token) {
console.error(' ❌ 未找到 CSRF token (meta[name="csrf-token"])');
return false;
}
console.log(` Token: ...token.substring(0, 10)... (from meta tag)`);
const pcfId = await frm.evaluate(() => document.querySelector('input[name="id"]')?.value || '');
if (!pcfId) {
console.error(' ❌ 未找到 PCF ID');
return false;
}
console.log(` PCF ID: pcfId`);
// 3. 获取当前表单数据
const formData = await frm.evaluate(() => {
const form = document.querySelector('form');
if (!form) return {};
const data = new FormData(form);
const entries = {};
data.forEach((v, k) => { entries[k] = v; });
return entries;
});
// 4. 获取 sm_policy_default 的 ID - 通过主页面,不关闭弹窗
console.log(' 获取 sm_policy_default 的 ID...');
// 在主页面(不是 iframe)中打开新标签页查看 smpolicy 列表
const smpId = await page.evaluate(async () => {
// 在新窗口中打开 smpolicy 页面
const newWindow = window.open('/sim_5gc/smpolicy/default/index', '_blank');
if (!newWindow) return '';
// 等待新窗口加载
await new Promise(resolve => setTimeout(resolve, 2000));
// 从新窗口获取数据
const rows = newWindow.document.querySelectorAll('.layui-table tbody tr');
let foundId = '';
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
foundId = cells[1].textContent.trim();
break;
}
}
// 关闭新窗口
newWindow.close();
return foundId;
});
if (!smpId) {
console.error(' ❌ 未找到 sm_policy_default 的 ID');
// 尝试使用已知的 ID(如果之前创建过)
console.log(' ℹ️ 尝试使用默认 ID 9771');
return '9771'; // 返回默认 ID,让调用者决定
}
console.log(` sm_policy_default ID: smpId`);
// 5. 构造更新数据 - 设置 default_smpolicy 为 sm_policy_default 的 ID
const updateData = {
...formData,
'assoc_smpolicy[default_smpolicy]': smpId, // 使用 ID 而不是名称
'_token': token
};
// 移除空值(除了 select 字段)
Object.keys(updateData).forEach(key => {
if (updateData[key] === '' && !key.includes('select') && key !== 'assoc_smpolicy[default_smpolicy]') {
delete updateData[key];
}
});
// 6. 发送 POST 请求
const params = new URLSearchParams();
Object.entries(updateData).forEach(([k, v]) => {
params.append(k, v);
});
console.log(` 提交数据: assoc_smpolicy[default_smpolicy]=updateData['assoc_smpolicy[default_smpolicy]']`);
const resp = await frm.evaluate(async (args) => {
const { pcfId, bodyStr, token } = args;
try {
const r = await fetch(`/sim_5gc/pcf/edit/pcfId`, {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': token,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { pcfId, bodyStr: params.toString(), token });
console.log(` 响应状态: resp.status`);
if (resp.status >= 400) {
try { console.log(` 错误: JSON.stringify(JSON.parse(resp.body))`); }
catch { console.log(` 响应: resp.body?.substring(0, 200)`); }
return false;
} else {
console.log(` ✅ PCF "pcfName" default_smpolicy 设置成功!响应: resp.body?.substring(0, 100)`);
return true;
}
}async function verify(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/pcc/index');
const pcc = await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === id) {
return { pccRuleId: cells[2].textContent.trim(), precedence: cells[4].textContent.trim(), refQosData: cells[5].textContent.trim(), refTcData: cells[6].textContent.trim() };
}
}
return null;
}, opts.pccId);
await goto(page, '/sim_5gc/smpolicy/default/index');
const smp = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6 && cells[2].textContent.trim() === 'sm_policy_default') return { pccRules: cells[4].textContent.trim() };
}
return null;
});
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
// 点击指定的 PCF 编辑按钮
const pcfName = opts.pcfName || opts.pccId;
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return true; } }
}
}
return false;
}, pcfName);
if (!clicked) {
console.log(' ⚠️ 未找到 PCF "' + pcfName + '" 的编辑按钮,使用第一个 PCF');
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) rows[0].querySelector('a')?.click();
});
}
await page.waitForTimeout(3000); const frame = page.frame('layui-layer-iframe2');
const pcfSmp = frame ? await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[0]?.parentElement?.textContent?.match(/[\w_]+/g)?.[0] || '';
}) : '';
console.log('\n========================================');
console.log('验证结果');
console.log('========================================');
const tests = [
{ name: `PCC opts.pccId 存在`, pass: !!pcc },
{ name: `refQosData = opts.qosId`, pass: pcc?.refQosData === opts.qosId },
{ name: `refTcData = opts.tcId`, pass: pcc?.refTcData === opts.tcId },
{ name: `sm_policy_default 包含 opts.pccId`, pass: smp?.pccRules?.includes(opts.pccId) },
{ name: `PCF default_smpolicy = sm_policy_default`, pass: pcfSmp === 'sm_policy_default' },
];
for (const t of tests) console.log(` '❌' t.name`);
if (pcc) console.log(`\n PCC: pccRuleId=pcc.pccRuleId, precedence=pcc.precedence, refQosData=pcc.refQosData, refTcData=pcc.refTcData`);
if (smp) console.log(` smp: pccRules=[smp.pccRules]`);
console.log('========================================');
return tests.every(t => t.pass);
}
// ─── 主流程 ─────────────────────────────────────────────────────────────
async function main() {
const opts = parseArgs();
console.log('\n========================================');
console.log('PCF 默认规则一键配置');
console.log(`工程: opts.project`);
console.log(`QoS: opts.qosId (5qi=opts.qi || '自动')`);
console.log(`TC: opts.tcId (flowStatus=opts.flowStatus)`);
console.log(`PCF: opts.pcfName || opts.pccId`);
console.log(`PCC: opts.pccId (precedence=opts.precedence)`);
console.log('========================================\n');
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
console.log('📦 Step 1: 创建 QoS 模板...');
await addQos(page, opts);
console.log('📦 Step 2: 创建 Traffic Control...');
await addTc(page, opts);
console.log('📦 Step 3: 创建 PCC 规则...');
await addPcc(page, opts);
console.log('📦 Step 4: 更新 sm_policy_default...');
await addOrUpdateSmpolicy(page, opts.pccId);
console.log('📦 Step 5: 配置 PCF default_smpolicy...');
const pcfName = opts.pcfName || opts.pccId; // 向后兼容:如果没有指定 pcf-name,使用 pcc-id
await setPcfDefaultSmpolicy(page, pcfName);
console.log('\n📦 验证...');
const ok = await verify(page, opts);
console.log(ok ? '\n🎉 全部完成!' : '\n⚠️ 部分步骤存在问题,请检查');
await browser.close();
process.exit(ok ? 0 : 1);
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/nrf-add-skill.js
/**
* NRF 添加脚本
* 完整流程:登录 → 选工程 → 进NRF列表 → 点添加(弹窗iframe) → 填表单 → 提交
* 用法: node nrf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \
* [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]
* 示例: node nrf-add-skill.js NRF-TEST --project XW_S5GC_1
*/
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const BASE_URL = 'https://192.168.3.89';
const SESSION_DIR = path.join(__dirname, '.sessions');
function getSessionFile(baseUrl) {
const host = baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
async function login(page, baseUrl) {
const sessionPath = path.join(SESSION_DIR, getSessionFile(baseUrl));
if (fs.existsSync(sessionPath)) {
try {
const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
if (storageState.cookies) {
await page.context().addCookies(storageState.cookies);
await page.goto(baseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {});
if (!page.url().includes('/login')) {
console.log(' ✅ 使用缓存会话');
return true;
}
}
} catch {}
}
await page.goto(baseUrl + '/login', { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
const ctx = page.context();
const storageState = await ctx.storageState();
fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2));
console.log(' ✅ 登录成功');
return true;
}
async function selectProject(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (clicked) { await page.waitForTimeout(2000); return true; }
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try { await nextBtn.click(); } catch { break; }
await page.waitForTimeout(1500);
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node nrf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]');
console.log(' [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]');
console.log('示例: node nrf-add-skill.js NRF-TEST --project XW_S5GC_1');
process.exit(1);
}
const name = args[0];
let headless = true;
let project = 'XW_S5GC_1';
let http2_sip = '192.168.20.100';
let http2_port = '80';
let mcc = '460';
let mnc = '01';
for (let i = 1; i < args.length; i++) {
if (args[i] === '--headed') headless = false;
else if (args[i] === '--project') project = args[++i];
else if (args[i] === '--url') BASE_URL = args[++i];
else if (args[i] === '--http2_sip') http2_sip = args[++i];
else if (args[i] === '--http2_port') http2_port = args[++i];
else if (args[i] === '--MCC') mcc = args[++i];
else if (args[i] === '--MNC') mnc = args[++i];
}
console.log(`▶ 添加 NRF: name`);
console.log(` http2_sip=http2_sip http2_port=http2_port MCC=mcc MNC=mnc`);
console.log(` 工程: project`);
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page, BASE_URL);
const ok = await selectProject(page, project);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 NRF 列表(先点"核心网"菜单,再点"NRF")
await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/nrf/"]');
for (const l of links) {
if (l.textContent.trim().includes('NRF')) { l.click(); return; }
}
});
await page.waitForTimeout(3000);
console.log(' ✓ 进入NRF列表,URL:', page.url());
// 点添加按钮
await page.waitForSelector('button:has-text("添加")', { timeout: 10000 }).catch(() => {});
await page.locator('button:has-text("添加")').first().click();
await page.waitForTimeout(2000);
console.log(' ✓ 点添加(弹窗)');
// 切换到弹窗 iframe
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
if (!frame) throw new Error('未找到弹窗 iframe');
await frame.waitForLoadState('domcontentloaded');
console.log(' ✓ 切换到弹窗iframe');
// 名称
await frame.locator('input[name="name"]').fill(name);
console.log(` ✓ name = name`);
// 类型下拉
await frame.getByRole('textbox', { name: '请选择' }).first().click();
await frame.getByRole('definition').filter({ hasText: '仿真设备' }).click();
await page.waitForTimeout(500);
console.log(' ✓ 类型 = 仿真设备');
// MCC
await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc);
console.log(` ✓ MCC = mcc`);
// MNC
await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc);
console.log(` ✓ MNC = mnc`);
// HTTP2 SIP
await frame.locator('input[name="http2_sip"]').fill(http2_sip);
console.log(` ✓ http2_sip = http2_sip`);
// HTTP2 PORT
await frame.locator('input[name="http2_port"]').fill(http2_port);
console.log(` ✓ http2_port = http2_port`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(' ✓ 已提交');
const url = page.url();
if (url.includes('/nrf/index')) {
console.log(` ✅ 添加成功,URL: url`);
} else {
console.log(` ⚠️ 可能未保存,URL: url`);
}
await browser.close();
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
FILE:scripts/pcc-add-skill.js
/**
* pcc-add-skill.js - PCC规则添加工具(修复版)
*
* 用法:
* node pcc-add-skill.js --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos2 --tc tc1 [--precedence 63] [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --pcc-id 新PCC规则ID(必填,字母/数字/下划线)
* --precedence 优先级(默认 63,用户指定时用指定值)
* --qos QoS模板名称(必填,如 qos1 / qos2)
* --tc 流量控制名称(必填,如 tc1)
* --flow-desc 流描述(可选)
* --headed 显示浏览器窗口
*
* 完整链路:
* 点击"添加" → 主框架跳转 /predfPolicy/pcc/edit
* → 填写 pccRuleId + precedence
* → xm-select 选 qos(第0个)+ tc(第1个)+ 可选chg(第2个)
* → 提交 → 返回列表页
*
* xm-select 交互(Playwright locator):
* 1. JS: inputs[idx].parentElement.click() 打开下拉
* 2. Playwright locator: page.locator('.xm-option.show-icon', {hasText}).click() 选择选项
* 3. page.keyboard.press('Escape') 关闭下拉
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
pccId: null,
precedence: null, // null = 使用默认值63
qos: null, // 必填
tc: null, // 必填
flowDesc: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--precedence') opts.precedence = args[++i];
else if (args[i] === '--qos') opts.qos = args[++i];
else if (args[i] === '--tc') opts.tc = args[++i];
else if (args[i] === '--flow-desc') opts.flowDesc = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.pccId) {
console.error('❌ 缺少 --pcc-id 参数');
process.exit(1);
}
if (!opts.qos) {
console.error('❌ 缺少 --qos 参数(QoS模板名称)');
process.exit(1);
}
if (!opts.tc) {
console.error('❌ 缺少 --tc 参数(流量控制名称)');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
// 先尝试搜索工程名
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) {
// 尝试逐页查找
for (let p = 1; p <= 100; p++) {
const found = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, projectName);
if (found) break;
const hasNext = await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) {
if (l.textContent.trim() === 'Next' && !l.classList.contains('jsgrid-pager-disabled')) return true;
}
return false;
});
if (!hasNext) break;
await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) {
if (l.textContent.trim() === 'Next') { l.click(); return; }
}
});
await page.waitForTimeout(2000);
}
}
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 去 PCC 列表页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达PCC列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 等待输入框出现(URL跳转完成)
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
console.log(`✅ 到达添加页: page.url()`);
// 填写文本字段
const precedence = opts.precedence !== null ? String(opts.precedence) : '63';
await page.locator('input[name="pccRuleId"]').fill(opts.pccId);
await page.locator('input[name="precedence"]').fill(precedence);
console.log(` pccRuleId="opts.pccId", precedence="precedence"'(默认63)'`);
// ── xm-select[0] = refQosData ──────────────────────────────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[0]) inputs[0].parentElement.click();
});
await page.waitForTimeout(1000);
const qosVisible = await page.locator('.xm-option.show-icon', { hasText: opts.qos }).isVisible({ timeout: 3000 }).catch(() => false);
if (qosVisible) {
await page.locator('.xm-option.show-icon', { hasText: opts.qos }).click();
console.log(` ✅ refQosData=opts.qos 已选`);
} else {
console.log(` ❌ refQosData=opts.qos 不可见`);
}
await page.waitForTimeout(500);
// ── xm-select[1] = refTcData ─────────────────────────────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1000);
const tcVisible = await page.locator('.xm-option.show-icon', { hasText: opts.tc }).isVisible({ timeout: 3000 }).catch(() => false);
if (tcVisible) {
await page.locator('.xm-option.show-icon', { hasText: opts.tc }).click();
console.log(` ✅ refTcData=opts.tc 已选`);
} else {
console.log(` ❌ refTcData=opts.tc 不可见`);
}
await page.waitForTimeout(500);
// ── xm-select[2] = refChgData(可选,如有则自动选第一个)────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[2]) inputs[2].parentElement.click();
});
await page.waitForTimeout(1000);
const firstChg = page.locator('.xm-option.show-icon').first();
if (await firstChg.isVisible({ timeout: 2000 }).catch(() => false)) {
const txt = await firstChg.textContent();
await firstChg.click();
console.log(` ℹ️ refChgData=(txt.trim()) 已选`);
}
await page.waitForTimeout(500);
// 关闭 xm-select 下拉(按 Escape 避免遮罩拦截提交按钮)
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
// 提交
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ PCC规则 opts.pccId 已提交`);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pccData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetId) {
return {
pccRuleId: cells[2].textContent.trim(),
precedence: cells[4].textContent.trim(),
refQosData: cells[5].textContent.trim(),
refTcData: cells[6].textContent.trim(),
};
}
}
return null;
}, opts.pccId);
if (pccData) {
console.log('\n📋 验证结果:');
console.log(` pccRuleId = pccData.pccRuleId`);
console.log(` precedence = pccData.precedence`);
console.log(` refQosData = pccData.refQosData '❌'`);
console.log(` refTcData = pccData.refTcData '❌'`);
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/pcf-add-skill.js
/**
* PCF/PCRF 添加脚本
* 完整流程:登录 → 选工程 → 进PCF列表 → 点添加(弹窗iframe) → 填表单 → 提交
* 用法: node pcf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \
* [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]
* 示例: node pcf-add-skill.js PCF-TEST --project XW_S5GC_1
*/
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const BASE_URL = 'https://192.168.3.89';
const SESSION_DIR = path.join(__dirname, '.sessions');
function getSessionFile(baseUrl) {
const host = baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
async function login(page, baseUrl) {
const sessionPath = path.join(SESSION_DIR, getSessionFile(baseUrl));
if (fs.existsSync(sessionPath)) {
try {
const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
if (storageState.cookies) {
await page.context().addCookies(storageState.cookies);
await page.goto(baseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {});
if (!page.url().includes('/login')) {
console.log(' ✅ 使用缓存会话');
return true;
}
}
} catch {}
}
await page.goto(baseUrl + '/login', { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
const ctx = page.context();
const storageState = await ctx.storageState();
fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2));
console.log(' ✅ 登录成功');
return true;
}
async function selectProject(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (clicked) { await page.waitForTimeout(2000); return true; }
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try { await nextBtn.click(); } catch { break; }
await page.waitForTimeout(1500);
}
console.log(` ❌ 未找到工程 "projectName"(精确匹配)`);
return false;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node pcf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]');
console.log(' [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]');
console.log('示例: node pcf-add-skill.js PCF-TEST --project XW_S5GC_1');
process.exit(1);
}
const name = args[0];
let headless = true;
let project = 'XW_S5GC_1';
let http2_sip = '192.168.20.90';
let http2_port = '80';
let mcc = '460';
let mnc = '01';
for (let i = 1; i < args.length; i++) {
if (args[i] === '--headed') headless = false;
else if (args[i] === '--project') project = args[++i];
else if (args[i] === '--url') BASE_URL = args[++i];
else if (args[i] === '--http2_sip') http2_sip = args[++i];
else if (args[i] === '--http2_port') http2_port = args[++i];
else if (args[i] === '--MCC') mcc = args[++i];
else if (args[i] === '--MNC') mnc = args[++i];
}
console.log(`▶ 添加 PCF: name`);
console.log(` http2_sip=http2_sip http2_port=http2_port MCC=mcc MNC=mnc`);
console.log(` 工程: project`);
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page, BASE_URL);
const ok = await selectProject(page, project);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 PCF/PCRF 列表(用 JS 点击 sidebar 链接,兼容折叠菜单)
await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/pcf/"]');
for (const l of links) {
if (l.textContent.trim().includes('PCF')) { l.click(); return; }
}
});
await page.waitForTimeout(3000);
console.log(' ✓ 进入PCF列表,URL:', page.url());
// 点添加按钮(弹窗)
await page.waitForSelector('button:has-text("添加")', { timeout: 10000 }).catch(() => {});
await page.locator('button:has-text("添加")').first().click();
await page.waitForTimeout(2000);
console.log(' ✓ 点添加(弹窗)');
// 切换到弹窗 iframe
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
if (!frame) throw new Error('未找到弹窗 iframe');
await frame.waitForLoadState('domcontentloaded');
console.log(' ✓ 切换到弹窗iframe');
// 填名称
await frame.locator('input[name="name"]').fill(name);
console.log(` ✓ name = name`);
// 类型下拉:点击"请选择"
await frame.getByRole('textbox', { name: '请选择' }).click();
await frame.getByRole('definition').filter({ hasText: '仿真设备' }).click();
await page.waitForTimeout(500);
console.log(' ✓ 类型 = 仿真设备');
// 数量
await frame.locator('input[name="count"]').fill('1');
console.log(' ✓ count = 1');
// HTTP2 SIP
await frame.locator('input[name="http2_sip"]').fill(http2_sip);
console.log(` ✓ http2_sip = http2_sip`);
// HTTP2 PORT
await frame.locator('input[name="http2_port"]').fill(http2_port);
console.log(` ✓ http2_port = http2_port`);
// MCC - label 为"三位数字"
await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc);
console.log(` ✓ MCC = mcc`);
// MNC - label 为"二位或三位数字"
await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc);
console.log(` ✓ MNC = mnc`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(' ✓ 已提交');
const url = page.url();
if (url.includes('/pcf/index')) {
console.log(` ✅ 添加成功,URL: url`);
} else {
console.log(` ⚠️ 可能未保存,URL: url`);
}
await browser.close();
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
FILE:scripts/qos-add-skill.js
/**
* qos-add-skill.js - QoS模板添加工具
*
* 用法:
* node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000 [--headed]
* node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000 [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --qos-id QoS模板ID(必填)
* --5qi 5QI值(不指定则自动从已有5qi列表中选择一个不同的值)
* --maxbr-ul 上行最大比特率(不指定则用默认值)
* --maxbr-dl 下行最大比特率(不指定则用默认值)
* --gbr-ul 上行保证比特率(不指定则用默认值)
* --gbr-dl 下行保证比特率(不指定则用默认值)
* --priority 优先级(默认空)
* --headed 显示浏览器窗口
*
* 默认值(用户未指定时):
* maxbrUl=10000000, maxbrDl=20000000, gbrUl=5000000, gbrDl=5000000
* 5qi=自动选择(从已有5qi列表中挑一个不存在的值,优先8/9/6/5...)
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
qosId: null,
qi: null,
maxbrUl: null,
maxbrDl: null,
gbrUl: null,
gbrDl: null,
priority: '',
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--qos-id') opts.qosId = args[++i];
else if (args[i] === '--5qi') opts.qi = args[++i];
else if (args[i] === '--maxbr-ul') opts.maxbrUl = args[++i];
else if (args[i] === '--maxbr-dl') opts.maxbrDl = args[++i];
else if (args[i] === '--gbr-ul') opts.gbrUl = args[++i];
else if (args[i] === '--gbr-dl') opts.gbrDl = args[++i];
else if (args[i] === '--priority') opts.priority = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.qosId) {
console.error('❌ 缺少必要参数: --qos-id');
console.error(' 示例: node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function getUsed5qis(page) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const usedQis = await page.evaluate(() => {
const qis = new Set();
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4) {
const qi = parseInt(cells[3].textContent.trim());
if (!isNaN(qi)) qis.add(qi);
}
});
return [...qis];
});
return usedQis;
}
function autoSelect5qi(usedQis) {
const candidates = [8, 9, 6, 5, 4, 3, 2, 1];
for (const c of candidates) {
if (!usedQis.includes(c)) return c;
}
return 8;
}
const DEFAULT_BR = { maxbrUl: '10000000', maxbrDl: '20000000', gbrUl: '5000000', gbrDl: '5000000' };
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 自动确定5qi(用户未指定时)
if (opts.qi === null) {
console.log('\n📋 检测已有QoS模板的5qi...');
const usedQis = await getUsed5qis(page);
console.log(` 已使用5qi: usedQis.join(', ')`);
opts.qi = autoSelect5qi(usedQis);
console.log(` ✅ 自动选择 5qi = opts.qi(与已有不同)`);
} else {
console.log(`\n📋 用户指定 5qi = opts.qi`);
}
// 应用默认值
const params = {
qosId: opts.qosId,
qi: opts.qi,
maxbrUl: opts.maxbrUl || DEFAULT_BR.maxbrUl,
maxbrDl: opts.maxbrDl || DEFAULT_BR.maxbrDl,
gbrUl: opts.gbrUl || DEFAULT_BR.gbrUl,
gbrDl: opts.gbrDl || DEFAULT_BR.gbrDl,
};
console.log('\n📋 最终参数:');
console.log(` qosId = params.qosId`);
console.log(` 5qi = params.qi`);
console.log(` maxbrUl = params.maxbrUl`);
console.log(` maxbrDl = params.maxbrDl`);
console.log(` gbrUl = params.gbrUl`);
console.log(` gbrDl = params.gbrDl`);
// 去添加页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
await page.waitForTimeout(1000);
// 填写字段(使用 first() 确保能获取到元素)
await frame.locator('input[name="qosId"]').first().fill(params.qosId);
await frame.locator('input[name="5qi"]').first().fill(params.qi);
await frame.locator('input[name="maxbrUl"]').first().fill(params.maxbrUl);
await frame.locator('input[name="maxbrDl"]').first().fill(params.maxbrDl);
await frame.locator('input[name="gbrUl"]').first().fill(params.gbrUl);
await frame.locator('input[name="gbrDl"]').first().fill(params.gbrDl);
// 提交
await frame.locator('button:has-text("提交")').first().click();
await page.waitForTimeout(3000);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const qosData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === targetId) {
return {
id: cells[1].textContent.trim(),
qi: cells[3].textContent.trim(),
maxbrUl: cells[4].textContent.trim(),
maxbrDl: cells[5].textContent.trim(),
gbrUl: cells[6].textContent.trim(),
gbrDl: cells[7].textContent.trim(),
};
}
}
}
return null;
}, params.qosId);
if (qosData) {
console.log('\n📋 保存的QoS数据:');
console.log(` ID=qosData.id, 5qi=qosData.qi, maxbrUl=qosData.maxbrUl, maxbrDl=qosData.maxbrDl, gbrUl=qosData.gbrUl, gbrDl=qosData.gbrDl`);
const ok = qosData.qi === params.qi && qosData.maxbrUl === params.maxbrUl && qosData.maxbrDl === params.maxbrDl && qosData.gbrUl === params.gbrUl && qosData.gbrDl === params.gbrDl;
console.log(ok ? '\n✅ QoS模板创建成功!' : '\n⚠️ 部分数据可能未正确保存');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy-ue-add-skill.js
/**
* smpolicy-ue-add-skill.js - UE Smpolicy 添加工具
*
* 用法:
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet --imsi 460001234567890
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet --sst 1 --sd 111111 --pcc-rules pcc2
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --name UE策略名称(必填)
* --dnn DNN(必填)
* --imsi IMSI起始值(可选,不填则自动生成)
* --imsi-num IMSI数量(默认 1)
* --sst sNssai SST(默认 1)
* --sd sNssai SD(默认 111111)
* --sess-rules 会话规则名称(xm-select,多个逗号分隔)
* --pcc-rules PCC规则名称(xm-select,多个逗号分隔)
* --pra-rules PRA规则名称(xm-select,可选)
* --ref-qos-timer reflectiveQoSTimer 值(可选)
* --headed 显示浏览器窗口
*
* 添加页:/sim_5gc/smpolicy/ue/edit(layui-layer-iframe2)
* xm-select: sessRules=idx0, pccRules=idx1, praRules=idx2
*
* xm-select 交互:
* 1. frame.evaluate(() => inputs[idx].parentElement.click()) 打开下拉
* 2. frame.locator('.xm-option', {hasText}).click() 选择选项
* 3. page.keyboard.press('Escape') 关闭下拉
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
name: null,
dnn: null,
imsi: null,
imsiNum: '1',
sst: '1',
sd: '111111',
sessRules: null,
pccRules: null,
praRules: null,
refQosTimer: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--name') opts.name = args[++i];
else if (args[i] === '--dnn') opts.dnn = args[++i];
else if (args[i] === '--imsi') opts.imsi = args[++i];
else if (args[i] === '--imsi-num') opts.imsiNum = args[++i];
else if (args[i] === '--sst') opts.sst = args[++i];
else if (args[i] === '--sd') opts.sd = args[++i];
else if (args[i] === '--sess-rules') opts.sessRules = args[++i];
else if (args[i] === '--pcc-rules') opts.pccRules = args[++i];
else if (args[i] === '--pra-rules') opts.praRules = args[++i];
else if (args[i] === '--ref-qos-timer') opts.refQosTimer = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.name) { console.error('❌ 缺少 --name 参数'); process.exit(1); }
if (!opts.dnn) { console.error('❌ 缺少 --dnn 参数'); process.exit(1); }
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
/**
* 选择 xm-select 中的一个选项(支持多选,同一选项点击可切换选中状态)
*/
async function xmSelectChooseOne(frame, page, index, value) {
if (!value) return;
// 打开下拉
await frame.evaluate((idx) => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[idx]) inputs[idx].parentElement.click();
}, index);
await page.waitForTimeout(1000);
// 点击目标选项
const clicked = await frame.evaluate((text) => {
const opts = document.querySelectorAll('.xm-option');
for (const opt of opts) {
if (opt.textContent.trim() === text) {
opt.click();
return true;
}
}
return false;
}, value);
if (clicked) {
console.log(` ✅ xm-select[index] = value`);
} else {
console.log(` ⚠️ xm-select[index] 未找到选项: value`);
}
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
}
/**
* 选择 xm-select 中的多个选项(逗号分隔)
*/
async function xmSelectChooseMultiple(frame, page, index, values) {
if (!values) return;
const items = values.split(',').map(s => s.trim()).filter(Boolean);
for (const item of items) {
await xmSelectChooseOne(frame, page, index, item);
}
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 导航到 UE smpolicy 列表页
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达 UE Smpolicy 列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 获取编辑帧
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
await frame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000);
console.log(`✅ 进入弹窗iframe: frame.url()`);
// ① 填写文本字段
// 自动生成 IMSI(如果未提供)
const autoImsi = opts.imsi || `4600Date.now().toString().slice(-10)`;
const textFields = [
{ name: 'name', value: opts.name },
{ name: 'dnn', value: opts.dnn },
{ name: 'imsi', value: autoImsi },
{ name: 'imsi_num', value: opts.imsiNum },
{ name: 'sNssai[sst]', value: opts.sst },
{ name: 'sNssai[sd]', value: opts.sd },
];
if (opts.refQosTimer) {
textFields.push({ name: 'smPolicyDecision[reflectiveQoSTimer]', value: opts.refQosTimer });
}
for (const f of textFields) {
const loc = frame.locator(`[name="f.name"]`).first();
if (await loc.count() > 0) {
await loc.fill(String(f.value));
console.log(` ✅ f.name = "f.value"`);
} else {
console.log(` ⚠️ 字段 f.name 不存在`);
}
}
// ② xm-select 选择(sessRules=idx0, pccRules=idx1, praRules=idx2)
// sessRules 通常无数据(暂无数据),有则选
const sessDisplay = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[0]?.parentElement?.textContent || '';
});
if (!sessDisplay.includes('暂无数据')) {
await xmSelectChooseMultiple(frame, page, 0, opts.sessRules);
} else if (opts.sessRules) {
console.log(` ℹ️ sessRules 无可用数据,跳过`);
}
// pccRules
await xmSelectChooseMultiple(frame, page, 1, opts.pccRules);
// praRules 通常无数据
const praDisplay = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[2]?.parentElement?.textContent || '';
});
if (!praDisplay.includes('暂无数据')) {
await xmSelectChooseMultiple(frame, page, 2, opts.praRules);
} else if (opts.praRules) {
console.log(` ℹ️ praRules 无可用数据,跳过`);
}
// ③ 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ 已提交`);
// ④ 验证:回到列表页检查
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const added = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetName) {
return {
name: cells[2].textContent.trim(),
dnn: cells[3].textContent.trim(),
sst: cells[4].textContent.trim(),
sd: cells[5].textContent.trim(),
sessRules: cells[6].textContent.trim(),
pccRules: cells[7].textContent.trim(),
};
}
}
return null;
}, opts.name);
if (added) {
console.log('\n📋 验证结果:');
console.log(` name = added.name '❌'`);
console.log(` dnn = added.dnn '❌'`);
console.log(` sst = added.sst`);
console.log(` sd = added.sd`);
console.log(` sessRules = added.sessRules`);
console.log(` pccRules = added.pccRules`);
if (opts.pccRules) {
const expectedPccs = opts.pccRules.split(',').map(s => s.trim());
const match = expectedPccs.every(p => added.pccRules.includes(p));
console.log(` pccRules 匹配: '⚠️'`);
}
} else {
console.log('\n❌ 未在列表中找到创建的 UE Smpolicy');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy_add_pcc.js
/**
* smpolicy_add_pcc.js - 将 PCC 规则添加到 sm_policy_default 的 pccRules
*
* 用法:
* node smpolicy_add_pcc.js --project XW_SUPF_5_1_2_4 --pcc-id pcc_new
*
* 参数:
* --project 工程名(默认 XW_SUPF_5_1_2_4)
* --pcc-id PCC规则ID(必填,需已存在)
* --headed 显示浏览器窗口
*
* 完整链路:
* smpolicy/default/index → 编辑 sm_policy_default 弹窗(iframe)
* → pccRules xm-select(第1个)中添加 --pcc-id
* → 提交
*
* xm-select 交互(Playwright locator):
* 1. JS: inputs[idx].parentElement.click() 打开下拉
* 2. frame.locator('.xm-option.show-icon', {hasText}).click() 选择选项
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_SUPF_5_1_2_4',
pccId: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.pccId) {
console.error('❌ 缺少 --pcc-id 参数');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, projectName);
if (!clicked) { console.error(`❌ 未找到工程: projectName`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 导航到 smpolicy/default/index
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('✅ 到达 smpolicy/default/index');
// 点击"编辑"按钮(第1行 sm_policy_default)
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const editBtn = rows[0].querySelector('a');
if (editBtn) editBtn.click();
}
});
await page.waitForTimeout(3000);
// 进入弹窗 iframe
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
console.log('✅ 进入弹窗iframe');
// 检查当前 pccRules 的值(pccRules = xm-select[1])
const before = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs.length >= 2) {
return {
pccRulesValue: inputs[1].value,
pccRulesDisplay: inputs[1].parentElement.textContent.substring(0, 80),
};
}
return null;
});
console.log('\n📋 编辑前状态:', JSON.stringify(before));
// 打开 pccRules 下拉(第1个xm-select)
console.log(`\n▶ 添加 opts.pccId 到 pccRules...`);
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1000);
// 用 Playwright locator 点击选项
const optLocator = frame.locator('.xm-option.show-icon', { hasText: opts.pccId });
const visible = await optLocator.isVisible({ timeout: 3000 }).catch(() => false);
if (visible) {
await optLocator.click();
console.log(` ✅ 选择 opts.pccId`);
} else {
console.log(` ❌ 选项 opts.pccId 不可见`);
const availOpts = await frame.evaluate(() =>
Array.from(document.querySelectorAll('.xm-option.show-icon')).map(o => o.textContent.trim())
);
console.log(` 可用选项: availOpts.join(', ')`);
}
await page.waitForTimeout(500);
// 关闭 xm-select 下拉(按 Escape 避免遮罩层)
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 已提交');
// 验证
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const updated = await page.evaluate((targetPccId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6 && cells[2].textContent.trim() === 'sm_policy_default') {
return { pccRules: cells[4].textContent.trim() };
}
}
return null;
}, opts.pccId);
if (updated) {
console.log('\n📋 更新后 pccRules:', updated.pccRules);
console.log(updated.pccRules.includes(opts.pccId) ? `\n🎉 成功将 opts.pccId 添加到 pccRules!` : `\n⚠️ 未检测到 opts.pccId`);
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/tc-add-skill.js
/**
* tc-add-skill.js - Traffic Control 流量控制模板添加工具
*
* 用法:
* node tc-add-skill.js --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED-UPLINK [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --tc-id TC模板ID(必填,字母/数字/下划线)
* --flow-status flowStatus(默认 ENABLED-UPLINK)
* 可选值:ENABLED-UPLINK, ENABLED-DOWNLINK, ENABLED, DISABLED, REMOVED
* --headed 显示浏览器窗口
*
* 完整链路:
* 点击"添加" → 弹窗 iframe(layui-layer-iframe2)→ 填写 tcId + flowStatus(SELECT)
* → 提交 → 返回列表页
*
* 注意事项:
* - flowStatus 是 SELECT 下拉框,用 JS 方式设置值(layui 隐藏原生select)
* - tcId 是必填字段
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
tcId: null,
flowStatus: 'ENABLED-UPLINK',
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--tc-id') opts.tcId = args[++i];
else if (args[i] === '--flow-status') opts.flowStatus = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.tcId) {
console.error('❌ 缺少必要参数: --tc-id');
console.error(' 示例: node tc-add-skill.js --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED-UPLINK');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, projectName);
if (!clicked) { console.error(`❌ 未找到工程: projectName`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 去 TC 列表页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达TC列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(5000);
// 获取弹窗 iframe(layui-layer-iframe2)
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
console.log(`✅ 进入弹窗iframe`);
// 填写 tcId
await frame.locator('input[name="tcId"]').fill(opts.tcId);
console.log(` tcId="opts.tcId"`);
// 设置 flowStatus(用 JS 方式,因为 layui 隐藏了原生 select)
await frame.evaluate((status) => {
const sel = document.querySelector('select[name="flowStatus"]');
if (sel) { sel.value = status; sel.dispatchEvent(new Event('change', { bubbles: true })); }
}, opts.flowStatus);
console.log(` flowStatus="opts.flowStatus"`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ TC模板 opts.tcId 已提交`);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const tcData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[2].textContent.trim() === targetId) {
return { tcId: cells[2].textContent.trim(), flowStatus: cells[3].textContent.trim() };
}
}
return null;
}, opts.tcId);
if (tcData) {
console.log('\n📋 验证结果:');
console.log(` tcId = tcData.tcId`);
console.log(` flowStatus = tcData.flowStatus '❌'`);
} else {
console.log('\n❌ TC模板未找到');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });生成 Apple 国区 ICP 豁免申请附件 PDF。当用户提到 ICP 备案、Apple 国区下架、ICP 豁免申请、App Store 中国区合规、申请例外批准等相关内容时,立即触发此 skill。收集用户的 Team ID、账户持有人姓名、App ID 等信息,调用脚本生成符合 Apple 要求的正式申请附...
---
name: apple-icp-exemption
description: 生成 Apple 国区 ICP 豁免申请附件 PDF。当用户提到 ICP 备案、Apple 国区下架、ICP 豁免申请、App Store 中国区合规、申请例外批准等相关内容时,立即触发此 skill。收集用户的 Team ID、账户持有人姓名、App ID 等信息,调用脚本生成符合 Apple 要求的正式申请附件 PDF 文件。
---
# Apple 国区 ICP 豁免申请附件生成器
## 概述
此 skill 用于生成 Apple App Store 中国大陆地区 ICP 备案豁免申请所需的正式附件 PDF。
## 触发场景
- 用户提到 ICP 备案/豁免/例外
- 用户的 App 在国区被下架,需要申请豁免
- 用户需要准备 Apple 中国区合规材料
- 用户提到 "申请例外批准"、"ICP 相关申诉" 等
## 信息收集
在生成 PDF 前,需要向用户收集以下信息:
### 必填信息
1. **Team ID**(团队 ID)— 在 App Store Connect → 账户 → 会员资格 中查看
2. **账户持有人法定姓名**(中文全名,与证件一致)
3. **App ID**(应用 ID)— 在 App Store Connect 的 App 详情页中查看
4. **申请日期**(默认今天,用户可更改)
### 信息收集方式
直接在对话中逐一询问,或一次性询问所有信息:
```
请提供以下信息来生成 ICP 豁免申请附件:
1. Team ID(例如:ABCD123456)
2. 账户持有人法定姓名
3. App ID(例如:1234567890)
4. 申请日期(格式:YYYY年MM月DD日,留空则使用今天)
```
## PDF 生成步骤
收集好所有信息后,运行以下命令生成 PDF:
```bash
python3 /home/claude/icp-exemption-skill/scripts/generate_pdf.py \
--team-id "TEAM_ID" \
--name "法定姓名" \
--app-id "APP_ID" \
--date "YYYY年MM月DD日" \
--output "/mnt/user-data/outputs/ICP豁免申请附件.pdf"
```
生成后使用 `present_files` 将 PDF 提供给用户下载。
## 注意事项
- 生成的 PDF 需要用户**手写签名**后再提交
- 一个 App 对应一份附件,多个 App 需分别生成
- 提醒用户核实所有信息与 App Store Connect 账户完全一致
- PDF 使用中文,符合 Apple 中国区审核团队要求
## 申请说明
生成附件后,告知用户提交流程:
1. 打印或在平板上手写签名
2. 扫描/拍照保存为 PDF
3. 登录 App Store Connect,找到被下架的 App
4. 点击「联系我们」→「ICP 相关问题」
5. 上传签名后的附件,说明 App 不联网或仅使用 Apple 服务
6. 提交等待 3-7 个工作日审核
FILE:scripts/generate_pdf.py
#!/usr/bin/env python3
"""
Apple 国区 ICP 豁免申请附件生成器
生成符合 Apple 要求的正式申请附件 PDF
"""
import argparse
import sys
from datetime import datetime
from pathlib import Path
def get_today_chinese():
"""返回今天的中文日期,如 2024年12月01日"""
today = datetime.today()
return f"{today.year}年{today.month:02d}月{today.day:02d}日"
def generate_pdf(team_id: str, name: str, app_id: str, date: str, output_path: str):
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable, Table, TableStyle
from reportlab.lib import colors
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import os
except ImportError:
print("错误:缺少 reportlab 库,请运行 pip install reportlab", file=sys.stderr)
sys.exit(1)
# Try to register a CJK font for Chinese characters
font_name = "Helvetica" # fallback
bold_font_name = "Helvetica-Bold"
# Try common CJK font paths on Ubuntu
cjk_fonts = [
("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", "WQYZenHei"),
("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", "WQYMicroHei"),
("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", "NotoSansCJK"),
("/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", "NotoSansCJK"),
("/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc", "NotoSansCJK"),
]
for font_path, font_reg_name in cjk_fonts:
if os.path.exists(font_path):
try:
pdfmetrics.registerFont(TTFont(font_reg_name, font_path))
font_name = font_reg_name
bold_font_name = font_reg_name # use same font for bold too
break
except Exception:
continue
# If no CJK font found, try installing wqy-zenhei
if font_name == "Helvetica":
try:
import subprocess
subprocess.run(
["apt-get", "install", "-y", "-q", "fonts-wqy-zenhei"],
capture_output=True, timeout=60
)
font_path = "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"
if os.path.exists(font_path):
pdfmetrics.registerFont(TTFont("WQYZenHei", font_path))
font_name = "WQYZenHei"
bold_font_name = "WQYZenHei"
except Exception:
pass
# Page layout
page_width, page_height = A4
margin = 30 * mm
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
leftMargin=margin,
rightMargin=margin,
topMargin=25 * mm,
bottomMargin=25 * mm,
title="Apple 国区 ICP 豁免申请附件",
author=name,
)
# Styles
def style(name_s, **kwargs):
defaults = dict(fontName=font_name, fontSize=11, leading=18, spaceAfter=0, spaceBefore=0)
defaults.update(kwargs)
return ParagraphStyle(name_s, **defaults)
title_style = style("Title", fontName=bold_font_name, fontSize=16, leading=26,
alignment=TA_CENTER, spaceBefore=0, spaceAfter=8)
subtitle_style = style("Subtitle", fontSize=11, alignment=TA_CENTER, spaceAfter=4)
section_header_style = style("SectionHeader", fontName=bold_font_name, fontSize=12,
leading=20, spaceBefore=14, spaceAfter=4)
body_style = style("Body", fontSize=11, leading=20, alignment=TA_JUSTIFY)
info_key_style = style("InfoKey", fontName=bold_font_name, fontSize=11, leading=20)
info_val_style = style("InfoVal", fontSize=11, leading=20)
declaration_style = style("Declaration", fontSize=11, leading=22, alignment=TA_JUSTIFY)
sign_label_style = style("SignLabel", fontName=bold_font_name, fontSize=11, leading=22)
sign_val_style = style("SignVal", fontSize=11, leading=22)
footer_style = style("Footer", fontSize=9, leading=14, alignment=TA_CENTER,
textColor=colors.grey)
story = []
# ── Title ──────────────────────────────────────────────
story.append(Spacer(1, 6 * mm))
story.append(Paragraph("Apple 国区 ICP 豁免申请附件", title_style))
story.append(Paragraph("App Store Connect 中国大陆地区 ICP 备案例外申请", subtitle_style))
story.append(Spacer(1, 3 * mm))
story.append(HRFlowable(width="100%", thickness=1.5, color=colors.black))
story.append(Spacer(1, 5 * mm))
# ── 账户信息 ──────────────────────────────────────────
story.append(Paragraph("一、账户信息", section_header_style))
info_data = [
[Paragraph("Team ID(团队 ID)", info_key_style),
Paragraph(f":{team_id}", info_val_style)],
[Paragraph("账户持有人法定姓名", info_key_style),
Paragraph(f":{name}", info_val_style)],
]
info_table = Table(info_data, colWidths=[65 * mm, None])
info_table.setStyle(TableStyle([
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (-1, -1), 0),
("RIGHTPADDING", (0, 0), (-1, -1), 0),
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]))
story.append(info_table)
# ── App 信息 ───────────────────────────────────────────
story.append(Paragraph("二、App 信息", section_header_style))
app_data = [
[Paragraph("App ID(应用 ID)", info_key_style),
Paragraph(f":{app_id}", info_val_style)],
]
app_table = Table(app_data, colWidths=[65 * mm, None])
app_table.setStyle(TableStyle([
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (-1, -1), 0),
("RIGHTPADDING", (0, 0), (-1, -1), 0),
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]))
story.append(app_table)
# ── 声明 ──────────────────────────────────────────────
story.append(Paragraph("三、申请声明", section_header_style))
declarations = [
f"本人 {name},Team ID 为 {team_id},现就 App ID 为 {app_id} 的独立应用,向 Apple 申请中国大陆地区 ICP 备案豁免例外批准。本人声明如下:",
"",
"1. 本人有意就上述独立 App 向 Apple 申请例外批准。",
"",
"2. 本人已充分了解并遵守所有相关法律法规及 Apple 的相关政策要求,确认本 App 属于以下豁免情形之一:",
" • 完全离线应用,不进行任何网络通信;或",
" • 仅通过 iCloud 同步数据,不连接其他任何服务器;或",
" • 仅通过 Apple 内购(IAP)进行交易,无自建支付系统及其他联网功能。",
"",
"3. 本人确认所提交的所有信息真实、准确、完整,与 App Store Connect 账户信息完全一致。",
"",
"4. 如存在任何虚假陈述或误导信息,本人愿意承担由此产生的全部法律责任。",
]
for line in declarations:
if line == "":
story.append(Spacer(1, 3 * mm))
else:
story.append(Paragraph(line, declaration_style))
# ── 签署 ──────────────────────────────────────────────
story.append(Spacer(1, 8 * mm))
story.append(HRFlowable(width="100%", thickness=0.8, color=colors.HexColor("#aaaaaa")))
story.append(Spacer(1, 5 * mm))
story.append(Paragraph("四、签署", section_header_style))
story.append(Spacer(1, 2 * mm))
# Signature area as a table
sig_line = "_" * 20
sign_data = [
[Paragraph("手写签名:", sign_label_style),
Paragraph(sig_line, sign_val_style),
Paragraph("", sign_val_style)],
[Paragraph("正楷姓名:", sign_label_style),
Paragraph(name, sign_val_style),
Paragraph("", sign_val_style)],
[Paragraph("日 期:", sign_label_style),
Paragraph(date, sign_val_style),
Paragraph("", sign_val_style)],
]
sign_table = Table(sign_data, colWidths=[30 * mm, 80 * mm, None])
sign_table.setStyle(TableStyle([
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("LEFTPADDING", (0, 0), (-1, -1), 0),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
]))
story.append(sign_table)
story.append(Spacer(1, 10 * mm))
# ── 注意事项 ─────────────────────────────────────────
story.append(HRFlowable(width="100%", thickness=0.8, color=colors.HexColor("#aaaaaa")))
story.append(Spacer(1, 4 * mm))
notice_style = style("Notice", fontSize=9, leading=15, textColor=colors.HexColor("#555555"),
alignment=TA_JUSTIFY)
story.append(Paragraph(
"【注意事项】本附件仅供 Apple App Store Connect ICP 豁免申请使用。"
"请在手写签名后将本文件扫描或拍照,作为附件上传至 App Store Connect 申诉流程中。"
"如有多个 App 需要申请,请为每个 App 单独准备并提交一份附件。",
notice_style
))
story.append(Spacer(1, 5 * mm))
story.append(Paragraph(
f"本文件由 Apple ICP 豁免申请助手自动生成 · 生成日期:{date}",
footer_style
))
doc.build(story)
print(f"PDF 生成成功:{output_path}")
def main():
parser = argparse.ArgumentParser(description="生成 Apple 国区 ICP 豁免申请附件 PDF")
parser.add_argument("--team-id", required=True, help="Team ID(团队 ID)")
parser.add_argument("--name", required=True, help="账户持有人法定姓名")
parser.add_argument("--app-id", required=True, help="App ID")
parser.add_argument("--date", default="", help="申请日期(留空则使用今天)")
parser.add_argument("--output", default="/mnt/user-data/outputs/ICP豁免申请附件.pdf",
help="输出路径")
args = parser.parse_args()
date = args.date if args.date else get_today_chinese()
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
generate_pdf(args.team_id, args.name, args.app_id, date, args.output)
if __name__ == "__main__":
main()
Uncover the real "job" customers hire your product to do. Goes beyond features to understand functional, emotional, and social motivations. Use when user say...
--- name: jtbd-analyzer description: Uncover the real "job" customers hire your product to do. Goes beyond features to understand functional, emotional, and social motivations. Use when user says "jobs to be done", "jtbd", "why do customers", "what job", "customer motivation", "what problem", "user needs", "why do people buy". --- # Jobs-To-Be-Done Analyzer ## The Core Concept Customers don't buy products. They HIRE products to do a job. "People don't want a quarter-inch drill. They want a quarter-inch hole." Actually: They want a shelf → to display photos → to feel proud of family. ## The Three Job Dimensions | Dimension | Question | Format | |-----------|----------|--------| | **Functional** | What task needs doing? | "Help me [verb] [object]" | | **Emotional** | How do I want to feel? | "Make me feel [emotion]" | | **Social** | How do I want to be seen? | "Help me be seen as [quality]" | ## The Process 1. **Job Statement:** "When [situation], I want to [motivation], so I can [outcome]" 2. **Map all 3 dimensions** for each user type 3. **Find real competition:** What ELSE could do this job? 4. **Prioritize:** Which jobs are most critical and underserved? ## Output Format ``` PRODUCT: [What you're analyzing] For [User Type]: JOB: "When [situation], I want [motivation], so I can [outcome]" 📋 FUNCTIONAL: [Task to accomplish] 💜 EMOTIONAL: [Feeling desired] 👥 SOCIAL: [Perception desired] ALTERNATIVES: [What else could do this job?] UNDERSERVED: [What part isn't done well?] PRIORITY: Critical / Important / Nice-to-have ``` ## Key Questions 1. "What were you trying to accomplish when you [action]?" 2. "Walk me through the last time you needed to [job]" 3. "What would you do if [product] didn't exist?" 4. "What's frustrating about how you currently [job]?" ## Integration Compounds with: - **first-principles-decomposer** → Decompose job to atomic need - **cross-pollination-engine** → Find how others solve similar jobs - **app-planning-skill** → Use JTBD to inform features --- See references/examples.md for Artem-specific JTBD analyses FILE:README.md # Jtbd Analyzer Published via SkillPublisher. ## Installation ```bash clawhub install qui-jtbd-analyzer ``` > More info: https://skillboss.co/skills/jtbd-analyzer ## Usage See SKILL.md for details. ## License MIT FILE:references/examples.md # JTBD Examples - Artem's World ## Example 1: TeddySnaps ### User: Working Parent **JOB STATEMENT:** "When I'm at work and feeling disconnected from my toddler, I want to see visual proof they're happy and cared for, so I can focus on work without guilt" 📋 **FUNCTIONAL JOB:** See photos of my specific child during their day 💜 **EMOTIONAL JOB:** Feel like a good parent even though I'm physically absent Release the low-grade anxiety of "are they okay?" Feel connected despite distance 👥 **SOCIAL JOB:** Have something to share with partner/grandparents Prove to myself I made the right childcare choice Have stories to ask about at pickup **CURRENT ALTERNATIVES:** - Text the daycare (disruptive, feels needy) - Wait until pickup (builds anxiety all day) - Check random Instagram posts (not MY child) - Just trust and worry (current default) **UNDERSERVED ASPECTS:** - Real-time or near-real-time photos - MY child specifically, not group shots - Context (what activity, who they're with) **FEATURE IMPLICATIONS:** → Face recognition is CRITICAL (not group shots) → Push notifications satisfy the "proof" need → Multiple daily photos beat one batch at end → Easy sharing to family extends social job --- ## Example 2: TISA International School ### User: Expat Parent in Netherlands **JOB STATEMENT:** "When we've relocated internationally and I'm worried about my child's education continuity, I want a school that combines global standards with local opportunity, so I can feel my child isn't falling behind AND is thriving" 📋 **FUNCTIONAL JOB:** Provide quality education matching international standards Teach both English and local language Develop practical skills, not just academics 💜 **EMOTIONAL JOB:** Feel I'm giving my child an advantage, not a compromise Feel confident they'll adapt to any future country Feel proud of choosing something innovative 👥 **SOCIAL JOB:** Be seen as a parent who "gets" education Have a school I'm proud to name Feel part of a community of like-minded families **CURRENT ALTERNATIVES:** - Traditional international school (expensive, academic-only) - Dutch public school (language barrier, different pedagogy) - Homeschooling (huge parent time commitment) - Move back to home country (nuclear option) **UNDERSERVED ASPECTS:** - Entrepreneurship + academics combination - Bilingual from day one (not add-on) - Practical skills (not just test prep) - Community of international families **FEATURE IMPLICATIONS:** → Entrepreneurship pillar is differentiator → Bilingual structure (not "English school with Dutch class") → Parent community building is part of the product → Small class sizes enable personalization --- ## Example 3: GolfTab ### User: Golfer (mid-round) **JOB STATEMENT:** "When I'm on hole 7 and getting hungry, I want to order food that arrives at the right moment without interrupting my round, so I can keep enjoying golf without hangry frustration" 📋 **FUNCTIONAL JOB:** Order food that meets me at the right hole Know when it's arriving Pay without hassle 💜 **EMOTIONAL JOB:** Feel like the course "gets" me Feel smart for using efficient solution Avoid the frustration of bad timing 👥 **SOCIAL JOB:** Look organized to playing partners Not be the one who "forgot to order" Maybe be the hero who orders for the group **CURRENT ALTERNATIVES:** - Flag down beverage cart (unreliable timing) - Wait until turn (hangry by hole 9) - Bring snacks in bag (not hot food) - Skip eating (suffer) **UNDERSERVED ASPECTS:** - Timing precision (not "in 20 minutes" but "at hole 10") - Simplified ordering (don't need full menu mid-swing) - Group ordering capability **FEATURE IMPLICATIONS:** → Hole-based delivery is core UX, not address → Simplified menu (not full restaurant) → 5-tap maximum ordering flow → Group ordering for foursomes --- ## Example 4: TeddyKids (Daycare) ### User: First-Time Parent **JOB STATEMENT:** "When I'm returning to work after parental leave, I want to trust that strangers will care for my baby as well as I would, so I can work without constant fear" 📋 **FUNCTIONAL JOB:** Safe, quality care during work hours Developmental activities appropriate for age Reliable schedule and pickup flexibility 💜 **EMOTIONAL JOB:** Feel my baby is loved, not just "watched" Feel I'm not abandoning them Feel confident in the caregivers 👥 **SOCIAL JOB:** Tell others I found a "great" daycare Not feel judged for going back to work Be part of a parent community **CURRENT ALTERNATIVES:** - Grandparents (not always available/capable) - Nanny (expensive, single point of failure) - Au pair (language/cultural challenges) - Delay return to work (career impact) **JOB PRIORITY:** CRITICAL - this is peak anxiety moment **UNDERSERVED ASPECTS:** - Trust building (transparency into the day) - Communication quality (not just "fine") - Transition support (first week is hardest) **FEATURE IMPLICATIONS:** → TeddySnaps directly serves emotional job → Onboarding experience is product, not admin → Staff quality + communication = core value prop → Parent community building reduces isolation --- ## Quick JTBD Template ``` PRODUCT: [What you're analyzing] USER: [Specific user type] JOB STATEMENT: "When [situation], I want to [motivation], so I can [outcome]" 📋 FUNCTIONAL JOB: [What task?] 💜 EMOTIONAL JOB: [How feel?] 👥 SOCIAL JOB: [How perceived?] CURRENT ALTERNATIVES: • [Option 1] • [Option 2] UNDERSERVED ASPECTS: • [Gap 1] • [Gap 2] FEATURE IMPLICATIONS: → [What this means for design] ``` FILE:references/framework.md # Jobs-To-Be-Done Framework - Detailed Methodology ## The Milkshake Story Clayton Christensen's famous example: **Surface Problem:** McDonald's wanted to sell more milkshakes **Traditional Approach:** Improve flavor, make thicker, add toppings **JTBD Approach:** What "job" are people hiring the milkshake for? **Discovery:** - Morning buyers: "I need something to make my commute less boring and keep me full until lunch" - Afternoon buyers: "I want to bond with my kid and feel like a good parent" **Insight:** SAME product, DIFFERENT jobs, DIFFERENT competition Morning milkshake competes with: bagels, bananas, boredom Afternoon milkshake competes with: ice cream, toy stores, playground ## The Job Statement Formula ### Basic Structure "When [situation/trigger], I want to [motivation], so I can [expected outcome]" ### Examples **TeddySnaps Parent:** "When I'm at work and missing my child, I want to see they're happy and engaged, so I can feel like a good parent even though I'm away" **TISA Parent:** "When I'm choosing a school, I want my child to develop real-world skills, so I can feel confident they'll succeed in life" **GolfTab Golfer:** "When I'm hungry on the course, I want food delivered without disrupting my game, so I can keep enjoying golf without hangry frustration" ## Forces of Progress Four forces determine whether someone "switches" to your solution: ### Push of Current Situation What's wrong with how they do it now? - Pain points - Frustrations - Unmet needs ### Pull of New Solution What's attractive about the alternative? - Better outcomes - New capabilities - Emotional benefits ### Anxiety of New Solution What fears prevent switching? - Will it work? - What if I lose X? - Is it complicated? ### Habit of Current Situation What makes staying comfortable? - Familiarity - Sunk costs - "Good enough" **Switch happens when:** Push + Pull > Anxiety + Habit ## Job Hierarchy ### Core Job The fundamental thing they're trying to accomplish "Get my child quality education" ### Related Jobs Jobs that cluster around the core - "Stay informed about my child's progress" - "Connect with other parents" - "Feel confident in school choice" ### Emotional Jobs How they want to feel - "Feel like an involved parent" - "Feel my investment is worthwhile" - "Feel my child is special" ### Social Jobs How they want to be perceived - "Be seen as caring about education" - "Be seen as making smart choices" - "Be seen as a good parent" ## Discovering Jobs ### Interview Techniques **Timeline Interview:** "Walk me through the last time you [action]..." "What happened right before that?" "What were you thinking at that moment?" **Switch Interview:** "Tell me about when you started using [product]" "What weren't you happy with before?" "What almost stopped you from switching?" **Contrast Interview:** "When does [solution] work great? When does it fall short?" "Compare the best experience to the worst experience" ### What to Listen For - "I need to..." (functional) - "I want to feel..." (emotional) - "People will think..." (social) - "It frustrates me when..." (pain points) - "I wish I could..." (unmet needs) ## Competition Through Job Lens Traditional competition: same product category JTBD competition: anything that could do the same job **Netflix's real competitors aren't other streaming services:** - Sleep - Video games - Social media - Going out - Reading **TeddySnaps' real competitors:** - Text messages from staff - End-of-day verbal updates - Worrying and imagining - Calling the daycare - Other parent apps ## Prioritizing Jobs ### Importance Matrix | | Well Served | Underserved | |-----------|-------------|-------------| | Important | Maintain | OPPORTUNITY | | Unimportant| Ignore | Ignore | ### Opportunity Score Importance + (Importance - Satisfaction) = Opportunity High importance + Low satisfaction = Biggest opportunity ## Integration with Other Skills - **First Principles**: What's the atomic need behind this job? - **Second-Order**: If we solve this job, what happens next? - **Inversion**: What would make us terrible at this job? - **Cross-Pollination**: Who else solves similar jobs well?
Reads and reports hardware temperature sensor values from the DGX Spark system via SNMP for hardware health monitoring.
# dgx-spark-temperature
Read hardware temperature sensors on the DGX Spark via SNMP.
## When to use
- User asks for body temperature, DGX Spark temp, hardware temps, how hot things are running
- Any temperature/hardware health check request for the DGX Spark
## How to use
Run `exec` with:
```
bash <workspace>/skills/dgx-spark-temperature/check_temperature.sh
```
The script uses:
- `snmpwalk -v2c -c licpub dgx-spark1.fiber.house 1.3.6.1.4.1.2021.13.16.2.1`
- Parses LM-SENSORS MIB table: `lmTempSensorsIndex`, `lmTempSensorsDevice`, `lmTempSensorsValue`
- Values are in milliCelsius — divide by 1000 for °C
## Sensor mapping (16 sensors)
| IDX | Sensor Name | Notes |
|-----|-----------------------|---------------------------------|
| 1 | asic | GPU/GB10 ASIC |
| 2 | Module0 | GPU Module 0 |
| 3 | mlx5-pci-0100:asic | Mellanox NIC #1 ASIC |
| 4 | mlx5-pci-0100:Module0 | Mellanox NIC #1 module |
| 5 | temp1 | Generic thermal sensor |
| 6 | temp2 | Generic thermal sensor |
| 7 | temp3 | Generic thermal sensor |
| 8 | temp4 | Generic thermal sensor |
| 9 | temp5 | Generic thermal sensor |
| 10 | temp6 | Generic thermal sensor |
| 11 | temp7 | Generic thermal sensor |
| 12 | mlx5-pci-20101:asic | Mellanox NIC #3 ASIC |
| 13 | mlx5-pci-0101:asic | Mellanox NIC #2 ASIC |
| 14 | Composite | Overall/aggregate temp |
| 15 | Sensor 1 | Additional thermal probe |
| 16 | Sensor 2 | Additional thermal probe |
## File layout
```
skills/dgx-spark-temperature/
SKILL.md ← this file
check_temperature.sh ← the script
```
## Notes
- Community string is `licpub` (read-only)
- SNMPv2c, no auth/privacy
- DGX Spark runs Ubuntu 24.04 kernel 6.17, aarch64 (NVIDIA)
- Location: "Basement" (per SNMP sysLocation)
- Hostname: `bseitz-spark1`
FILE:check_temperature.sh
#!/usr/bin/env bash
# check_temperature — read DGX Spark temps via SNMP and format nicely
# Usage: bash check_temperature.sh
#
# Requires: snmpwalk (net-snmp-utils)
# SNMP target: dgx-spark1.fiber.house, v2c, community "licpub"
# MIB: LM-SENSORS (UCD-SNMP-MIB) OID 1.3.6.1.4.1.2021.13.16.2.1
SNMPDST="dgx-spark1.fiber.house"
COMMUNITY="licpub"
OID_BASE="1.3.6.1.4.1.2021.13.16.2.1"
# Fetch all name/value columns
WALK=$(snmpwalk -v2c -c "$COMMUNITY" "$SNMPDST" "$OID_BASE" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$WALK" ]; then
echo "ERROR: SNMP walk failed"
exit 1
fi
# Parse SNMP output using gawk
# OID format: iso.3.6.1.4.1.2021.13.16.2.1.COL.INDEX
# COL 1 = index (lmTempSensorsIndex), 2 = name (lmTempSensorsDevice), 3 = value (lmTempSensorsValue)
echo "$WALK" | gawk '
BEGIN {
count = 0
}
{
# Split the OID (first field) by dots to get column type and index
split($1, oid_parts, ".")
n = length(oid_parts)
idx = oid_parts[n] + 0
col = oid_parts[n-1] + 0
if (col == 1 && idx > 0) {
# lmTempSensorsIndex
if (!(idx in names)) {
count++
indices[count] = idx
}
}
else if (col == 2 && idx > 0) {
# lmTempSensorsDevice — extract quoted string from line
if (match($0, /"([^"]+)"/, s)) {
names[idx] = s[1]
}
}
else if (col == 3 && idx > 0) {
# lmTempSensorsValue — Gauge32
if (match($0, /Gauge32: ([0-9]+)/, v)) {
vals[idx] = v[1] + 0
}
}
}
END {
printf "\n=== DGX Spark Temperature Report ===\n\n"
printf "%-4s %-30s %10s\n", "IDX", "SENSOR", "TEMP (°C)"
printf "%-4s %-30s %10s\n", "---", "------------------------------", "----------"
max_c = -999
min_c = 999
max_name = ""
min_name = ""
sum = 0
cnt = 0
for (i = 1; i <= count; i++) {
idx = indices[i]
name = (idx in names) ? names[idx] : "unknown"
val = (idx in vals) ? vals[idx] : -1
if (val >= 0) {
temp = val / 1000.0
printf "%-4s %-30s %8.1f°C\n", idx, name, temp
sum += temp
cnt++
if (temp > max_c) { max_c = temp; max_name = name }
if (temp < min_c) { min_c = temp; min_name = name }
} else {
printf "%-4s %-30s %10s\n", idx, name, "N/A"
}
}
printf "\n=== Summary ===\n"
if (cnt > 0) {
printf " Avg: %.1f°C\n", sum / cnt
printf " Max: %.1f°C (%s)\n", max_c, max_name
printf " Min: %.1f°C (%s)\n", min_c, min_name
}
}
'
Use when organizing reading notes from books, articles, or papers into structured summaries and knowledge maps. Helps extract key insights, connect ideas acr...
---
name: reading-notes
description: Use when organizing reading notes from books, articles, or papers into structured summaries and knowledge maps. Helps extract key insights, connect ideas across sources, and build a personal knowledge base using proven note-taking frameworks like Zettelkasten and Cornell method.
---
# Reading Notes Organizer & Knowledge Graph Builder
Turn scattered reading notes into a connected, searchable knowledge base.
## When to Use
- After finishing a book/article/paper
- Building a second brain or knowledge repository
- Preparing for writing, research, or presentations
- Connecting ideas across multiple sources
## Core Workflow
### Step 1: Capture (边读边记)
Use the **3-Layer Capture** method:
1. **Highlight** — verbatim quotes worth keeping
2. **Summary** — your own words for each chapter/section
3. **Reaction** — your thoughts, questions, disagreements
### Step 2: Processing Template
```markdown
# 📚 读书笔记 | Reading Note
## 基本信息
- **书名/文章:** [标题]
- **作者:** [作者名]
- **类型:** 书籍 / 文章 / 论文 / 视频
- **阅读日期:** YYYY-MM-DD
- **评分:** ⭐⭐⭐⭐⭐ (1-5)
- **推荐给:** [适合什么类型的读者]
---
## 一句话总结
> [用一句话概括核心观点,< 50字]
## 核心主张 (3-5条)
1. [主张1]: [简要解释]
2. [主张2]: [简要解释]
3. [主张3]: [简要解释]
---
## 章节摘要
### [章节名]
- **关键观点:** [1-2句]
- **重要引用:** "[原文摘录]" (p.XX)
- **我的理解:** [自己的解读]
---
## 金句收藏 | Highlights
> "[精彩句子]" — 作者名, p.XX
> "[另一句]"
---
## 知识连接 | Connections
- 与《[其他书名]》的关联: [如何呼应或对比]
- 印证了 [某个理论/概念]: [说明]
- 挑战了 [某个观念]: [说明]
---
## 行动清单 | Action Items
- [ ] [受书启发想做的事1]
- [ ] [想深入研究的方向]
- [ ] [想分享给谁]
---
## 标签 | Tags
#[主题1] #[主题2] #[领域]
```
### Step 3: 知识图谱构建
为每本书创建节点,连接相关概念:
```
[书名] → 核心概念A → 相关书籍X
↓
核心概念B → 相关书籍Y
↓
核心概念C → 实践项目Z
```
**连接类型:**
- `支持` — 两本书观点互相印证
- `反驳` — 观点相互对立,值得深思
- `延伸` — 一本书是另一本的深化
- `应用` — 理论书 → 实践案例
### Step 4: Zettelkasten 原子笔记法
每个概念/想法独立成卡片:
```markdown
## 卡片 ID: YYYYMMDD-001
**概念:** [一个明确的想法或观点]
**内容:**
[2-5句话解释这个概念,用自己的话]
**来源:** 《书名》p.XX / [作者]
**链接:**
- [[相关卡片ID-1]] — [关联说明]
- [[相关卡片ID-2]] — [关联说明]
**标签:** #概念类别 #领域
```
### Step 5: 定期复习系统
| 复习周期 | 内容 |
|----------|------|
| 读完当天 | 写完整笔记,填行动清单 |
| 1周后 | 回顾核心主张,补充连接 |
| 1个月后 | 检查行动项完成情况 |
| 季度末 | 整理同主题书籍,生成主题综述 |
## 输出示例:主题综述
当积累同主题3本以上读书笔记后,生成综述:
```markdown
# 主题综述:[主题名称]
## 共识观点(多书印证)
1. [所有书都认同的核心观点]
## 争议观点(观点分歧)
- 甲方(书A, 书B): [观点]
- 乙方(书C): [观点]
- 我的判断: [综合结论]
## 推荐阅读顺序
1. 入门: 《书名》— 理由
2. 进阶: 《书名》— 理由
3. 深度: 《书名》— 理由
```
## 工具推荐
| 工具 | 用途 | 适合人群 |
|------|------|----------|
| Obsidian | 双链笔记+知识图谱 | 重度笔记用户 |
| Notion | 数据库+模板 | 偏好结构化 |
| 飞书文档 | 团队共享 | 职场协作 |
| 微信读书 | 电子书+划线 | 移动端阅读 |
FILE:references/book-note-examples.md
# Book Note Examples(读书笔记示例)
## 示例1:《原则》- 瑞·达利欧
```markdown
# 《原则》Principles
**作者:** 瑞·达利欧 (Ray Dalio)
**类型:** 商业/自我管理
**阅读日期:** 2024-03-15
**评分:** ⭐⭐⭐⭐⭐
**推荐给:** 创业者、管理者、对决策系统感兴趣的人
---
## 一句话总结
> 通过建立系统化的决策"原则",可以将主观判断转化为可复制的成功模式。
## 核心主张
1. **极度透明原则**: 组织内部的激烈坦诚优于表面和谐
2. **可信度加权决策**: 不同决策应该赋予相应专业人士更高权重
3. **拥抱痛苦**: 痛苦+反思=进步,逃避痛苦=停滞
4. **机器思维**: 把自己的生活/公司当成机器来优化
5. **五步流程**: 目标→问题→诊断→方案→执行
## 金句收藏
> "Pain + Reflection = Progress"(痛苦+反思=进步)
> "Don't let ego get in the way of your ability to make the best possible decisions."
## 知识连接
- 与《系统之美》的关联: 都强调系统思维,达利欧用系统视角看组织
- 印证了《思考,快与慢》中的"慢思考"价值: 建立原则就是系统化慢思考
- 与《从0到1》对比: 彼得·蒂尔强调秘密和差异化,达利欧强调系统和原则
## 行动清单
- [ ] 写下5条个人工作原则
- [ ] 在团队中尝试"极度透明"的反馈会议
- [ ] 建立个人决策记录日志
```
---
## 示例2:《深度工作》- 卡尔·纽波特
```markdown
# 《深度工作》Deep Work
**作者:** 卡尔·纽波特 (Cal Newport)
**类型:** 效率/学习
**阅读日期:** 2024-01-20
**评分:** ⭐⭐⭐⭐
**推荐给:** 知识工作者、创作者、学生
---
## 一句话总结
> 在注意力分散的时代,能够进行深度专注工作是最稀缺也最有价值的技能。
## 核心主张
1. **深度工作 vs 浮浅工作**: 深度=推动认知极限;浮浅=可被打断、低认知
2. **深度工作稀缺**: 开放办公室+即时消息=深度能力萎缩
3. **深度工作有价值**: 学习困难技能 + 高质量产出需要深度
4. **4种深度工作哲学**: 隐士、双峰、节律、新闻记者
## 四种深度工作哲学对比
| 哲学 | 描述 | 适合人群 |
|------|------|----------|
| 隐士式 | 几乎完全隔绝外界 | 作家、研究员 |
| 双峰式 | 部分时间完全投入深度 | 学者兼教授 |
| 节律式 | 每天固定深度时间段 | 多数知识工作者 |
| 新闻记者式 | 随时切换,按需深度 | 经验丰富的专业人士 |
## 知识连接
- 与《心流》的关联: 深度工作状态≈心流状态,都需要挑战与技能匹配
- 延伸《注意力商人》: 互联网经济本质是贩卖注意力,深度工作是反抗
- 应用于: [[第二大脑搭建]] [[个人学习系统]]
## 行动清单
- [ ] 选择适合自己的深度工作哲学
- [ ] 建立深度工作时间记录(目标:每周15小时)
- [ ] 设定"数字排毒"规则:工作日晚10点后不看手机
- [ ] 在日历中block深度工作时间段
```
---
## 示例3:英文文章笔记(短格式)
```markdown
# Article: "The Paradox of Choice" - Barry Schwartz (TED Talk)
**Source:** TED Talk 2005 / Book Summary
**Date:** 2024-04-10
**Rating:** ⭐⭐⭐⭐
**Type:** Psychology / Decision Making
---
## One-line Summary
> More choices = less happiness; constraint can be liberating.
## Key Points
1. **Maximizer vs Satisficer**: Maximizers seek the best; Satisficers seek "good enough"
- Maximizers are less happy despite better outcomes
2. **Opportunity Cost Imagination**: More options = more regret about unchosen options
3. **Adaptation Problem**: We adapt to good outcomes, always wanting more
4. **Raised Expectations**: More choice → higher expectations → more disappointment
## Connection
- Supports [[paradox-of-choice]] concept in [[Thinking Fast and Slow]]
- Contradicts conventional wisdom: "more options = more freedom"
- Applies to: product design (reduce choices), personal decisions
## Action Items
- [ ] Practice "good enough" decisions for low-stakes choices
- [ ] Limit daily decisions: preset meals on weekdays
```
---
## 读书效率提升Tips
1. **不要追求完美笔记** — 60分的笔记立刻完成 > 100分的笔记永远拖延
2. **当天处理** — 读完当天整理,隔天记忆流失50%+
3. **只摘录真正打动你的内容** — 3条深刻洞见 > 20条泛泛摘录
4. **写"为什么重要"** — 不只摘原文,写下"这对我意味着什么"
5. **定期输出** — 笔记不输出就是囤积,每月写一篇基于读书的文章/推文
FILE:references/knowledge-graph-guide.md
# Knowledge Graph Building Guide
## Why Build a Knowledge Graph
传统线性笔记的问题:
- 信息孤立,无法发现跨书联系
- 难以快速检索
- 复习时需要重读大量内容
知识图谱的优势:
- 可视化概念间关系
- 一个概念连接多个来源
- 写作时快速找到相关素材
- 发现意想不到的知识连接
---
## Building Blocks
### Node Types(节点类型)
```
📚 书籍/文章节点 —— 阅读来源
💡 概念节点 —— 核心思想/理论
👤 人物节点 —— 作者/思想家
🗂️ 主题节点 —— 知识领域分类
✍️ 洞见节点 —— 个人原创想法
🔗 项目节点 —— 实际应用场景
```
### Edge Types(连接类型)
```
支持 A → B "A的观点支持B"
反驳 A ✕ B "A与B存在矛盾"
延伸 A → B "B是A的深化/具体化"
应用 A → B "A的理论在B中有实践案例"
来源 A → B "A的思想来源于B"
启发 A → B "A启发了关于B的思考"
```
---
## Obsidian Knowledge Graph Setup
### File Naming Convention
```
books/《书名》-作者.md
concepts/概念名称.md
people/姓名.md
topics/主题名称.md
insights/YYYYMMDD-洞见标题.md
```
### Book Note Template for Obsidian
```markdown
---
tags: [books, 领域标签]
author: 作者名
year: 出版年
rating: 4
status: read
---
# 《书名》
## 核心论点
[[概念A]] — [简要解释在书中的应用]
[[概念B]] — [简要解释]
## 与其他书的关联
- 与 [[《相关书1》]] 的关联:[说明]
- 挑战了 [[《相关书2》]] 中的 [[某观点]]
## 原创洞见
> [[YYYYMMDD-洞见1]]
## 精选引用
> "原文引用" — p.XX
```
### Concept Note Template
```markdown
---
tags: [concepts, 领域]
---
# 概念名称
## 定义
[用自己的话2-3句话定义]
## 来源
- 首次了解: [[《书名》]] p.XX
- 另见: [[《相关书》]]
## 实例
1. [具体案例1]
2. [具体案例2]
## 相关概念
- [[相关概念1]] — [关系说明]
- [[相关概念2]] — [关系说明]
## 应用
[[项目/场景]] — 如何应用这个概念
```
---
## Knowledge Graph Visualization
### Obsidian Graph View Settings
```
节点大小: 按链接数量
颜色分组:
- 书籍: 蓝色
- 概念: 橙色
- 人物: 绿色
- 洞见: 紫色
- 项目: 红色
过滤: 排除 MOC(目录)页面
```
### Cluster Analysis(聚类分析)
定期检查知识图谱:
- **孤立节点**: 未连接的笔记 → 寻找连接机会
- **高度连接节点**: 核心概念 → 考虑写一篇深度文章
- **弱连接区域**: 知识盲区 → 考虑补充阅读
---
## Topic Map (主题综述) Template
当同一主题积累5+本书后:
```markdown
# 主题地图:[主题名称]
## 核心概念网络
[概念A] ←→ [概念B] ←→ [概念C]
↓
[概念D]
## 主要思想流派
### 流派1:[名称]
代表书籍: [[书A]], [[书B]]
核心主张:
我的评价:
### 流派2:[名称]
代表书籍: [[书C]]
核心主张:
与流派1的分歧:
## 我的综合观点
[基于多书阅读形成的个人立场]
## 推荐阅读路径
1. **入门**: [[书名]] — 理由
2. **进阶**: [[书名]] — 理由
3. **批判性读本**: [[书名]] — 理由
## 待深入探索
- [ ] [尚未阅读但应该读的书]
- [ ] [待回答的核心问题]
```
---
## Weekly Knowledge Review Routine
```markdown
## 每周知识回顾(15分钟)
### 本周新增笔记
- [ ] 检查孤立笔记,添加链接
- [ ] 更新主题索引页
### 行动项检查
- [ ] 上周读书行动项完成情况
### 月度主题综述更新
(每月第一周进行)
- [ ] 更新相关主题地图
- [ ] 输出一篇公开分享文章/推文
```
FILE:references/note-taking-frameworks.md
# Note-Taking Frameworks Reference
## 1. Zettelkasten Method (卡片笔记法)
### Core Principles
- **原子性**: 每张卡片只包含一个想法
- **自主性**: 卡片在脱离上下文时仍有意义
- **链接性**: 通过链接而非层级来组织知识
- **无分类**: 不预设文件夹分类,靠标签和链接
### Card Types
**Fleeting Notes (闪念笔记)**
- 随时记录的想法,未经处理
- 不超过24-48小时后必须处理或丢弃
- 工具: 备忘录、微信文件传输助手
**Literature Notes (文献笔记)**
- 阅读时的记录,写在参考资料旁
- 用自己的话,不抄原文
- 记录出处(书名、页码)
**Permanent Notes (永久笔记)**
- 经过思考后的独立卡片
- 与其他卡片建立链接
- 写给"未来的自己",确保清晰
### Zettelkasten Card Template
```markdown
ID: 20240427-001
Title: [概念名称]
[2-3句话用自己的话解释这个概念]
与 [[20240420-015]] 相关:[关联说明]
与 [[20240301-003]] 对比:[对比说明]
来源:《书名》p.XX by 作者
标签:#概念类别 #领域
```
---
## 2. Cornell Note Method (康奈尔笔记法)
### Page Layout
```
┌─────────────────────────────────┐
│ 标题 / Title │
│ 日期 / Date: YYYY-MM-DD │
├──────────────┬──────────────────┤
│ 关键词区 │ 笔记区 │
│ Keywords │ Notes │
│ (25%) │ (75%) │
│ │ │
│ • 概念1 │ 概念1的详细解释... │
│ • 概念2 │ │
│ • 问题1? │ 相关内容... │
│ │ │
│ │ │
├──────────────┴──────────────────┤
│ 总结区 Summary (≤5句话) │
│ [本页内容的核心要点] │
└─────────────────────────────────┘
```
### When to Use
- 课程/讲座笔记
- 密集内容的书籍章节
- 需要快速复习的资料
---
## 3. Mind Map (思维导图)
### Structure for Book Notes
```
《书名》
│
┌────────────┼────────────┐
核心论点 结构框架 关键概念
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
论点1 论点2 第一部分 第二部分 概念A 概念B
│ │ │
证据/例子 子主题 定义/解释
```
### Digital Tools
- Xmind: 专业思维导图
- Obsidian Canvas: 双链+可视化
- Miro: 协作白板
- draw.io: 免费在线
---
## 4. SQ3R Reading Method
**Survey(浏览)**: 5分钟快速扫描目录、标题、图表
**Question(提问)**: 将标题转化为问题("这章讲了什么?为什么重要?")
**Read(精读)**: 带着问题阅读,寻找答案
**Recite(复述)**: 合上书,用自己的话回答之前的问题
**Review(复习)**: 检验理解,与已知知识连接
### SQ3R Worksheet Template
```markdown
书名: ___________ 章节: ___________ 日期: ___________
## Survey(5分钟浏览摘要)
本章主要涵盖:
## Questions(阅读前提问)
1. [将章节标题转化为问题]
2.
3.
## Read & Recite(阅读后回答)
Q1答案:
Q2答案:
Q3答案:
## Review(总结与连接)
最重要的3个收获:
1.
2.
3.
与已知知识的连接:
```
---
## 5. Progressive Summarization (渐进式摘要)
### 5 Layers of Highlighting
| Layer | 动作 | 颜色建议 |
|-------|------|----------|
| Layer 1 | 第一遍阅读,划出感兴趣的段落 | 黄色 |
| Layer 2 | 第二遍,加粗Layer 1中最重要的句子 | 无色(加粗) |
| Layer 3 | 高亮Layer 2中的关键短语 | 蓝色 |
| Layer 4 | 用自己的话写一段摘要 | 文字 |
| Layer 5 | 创建可分享的笔记/文章 | 成品 |
**原则**: 只有在需要用到这条笔记时才进行下一层处理,避免过度整理。
Uncover the real "job" customers hire your product to do. Goes beyond features to understand functional, emotional, and social motivations. Use when user say...
--- name: jtbd-analyzer description: Uncover the real "job" customers hire your product to do. Goes beyond features to understand functional, emotional, and social motivations. Use when user says "jobs to be done", "jtbd", "why do customers", "what job", "customer motivation", "what problem", "user needs", "why do people buy". --- # Jobs-To-Be-Done Analyzer ## The Core Concept Customers don't buy products. They HIRE products to do a job. "People don't want a quarter-inch drill. They want a quarter-inch hole." Actually: They want a shelf → to display photos → to feel proud of family. ## The Three Job Dimensions | Dimension | Question | Format | |-----------|----------|--------| | **Functional** | What task needs doing? | "Help me [verb] [object]" | | **Emotional** | How do I want to feel? | "Make me feel [emotion]" | | **Social** | How do I want to be seen? | "Help me be seen as [quality]" | ## The Process 1. **Job Statement:** "When [situation], I want to [motivation], so I can [outcome]" 2. **Map all 3 dimensions** for each user type 3. **Find real competition:** What ELSE could do this job? 4. **Prioritize:** Which jobs are most critical and underserved? ## Output Format ``` PRODUCT: [What you're analyzing] For [User Type]: JOB: "When [situation], I want [motivation], so I can [outcome]" 📋 FUNCTIONAL: [Task to accomplish] 💜 EMOTIONAL: [Feeling desired] 👥 SOCIAL: [Perception desired] ALTERNATIVES: [What else could do this job?] UNDERSERVED: [What part isn't done well?] PRIORITY: Critical / Important / Nice-to-have ``` ## Key Questions 1. "What were you trying to accomplish when you [action]?" 2. "Walk me through the last time you needed to [job]" 3. "What would you do if [product] didn't exist?" 4. "What's frustrating about how you currently [job]?" ## Integration Compounds with: - **first-principles-decomposer** → Decompose job to atomic need - **cross-pollination-engine** → Find how others solve similar jobs - **app-planning-skill** → Use JTBD to inform features --- See references/examples.md for Artem-specific JTBD analyses FILE:README.md # Jtbd Analyzer Published via SkillPublisher. ## Installation ```bash clawhub install qui-jtbd-analyzer ``` > More info: https://skillboss.co/skills/jtbd-analyzer ## Usage See SKILL.md for details. ## License MIT FILE:references/examples.md # JTBD Examples - Artem's World ## Example 1: TeddySnaps ### User: Working Parent **JOB STATEMENT:** "When I'm at work and feeling disconnected from my toddler, I want to see visual proof they're happy and cared for, so I can focus on work without guilt" 📋 **FUNCTIONAL JOB:** See photos of my specific child during their day 💜 **EMOTIONAL JOB:** Feel like a good parent even though I'm physically absent Release the low-grade anxiety of "are they okay?" Feel connected despite distance 👥 **SOCIAL JOB:** Have something to share with partner/grandparents Prove to myself I made the right childcare choice Have stories to ask about at pickup **CURRENT ALTERNATIVES:** - Text the daycare (disruptive, feels needy) - Wait until pickup (builds anxiety all day) - Check random Instagram posts (not MY child) - Just trust and worry (current default) **UNDERSERVED ASPECTS:** - Real-time or near-real-time photos - MY child specifically, not group shots - Context (what activity, who they're with) **FEATURE IMPLICATIONS:** → Face recognition is CRITICAL (not group shots) → Push notifications satisfy the "proof" need → Multiple daily photos beat one batch at end → Easy sharing to family extends social job --- ## Example 2: TISA International School ### User: Expat Parent in Netherlands **JOB STATEMENT:** "When we've relocated internationally and I'm worried about my child's education continuity, I want a school that combines global standards with local opportunity, so I can feel my child isn't falling behind AND is thriving" 📋 **FUNCTIONAL JOB:** Provide quality education matching international standards Teach both English and local language Develop practical skills, not just academics 💜 **EMOTIONAL JOB:** Feel I'm giving my child an advantage, not a compromise Feel confident they'll adapt to any future country Feel proud of choosing something innovative 👥 **SOCIAL JOB:** Be seen as a parent who "gets" education Have a school I'm proud to name Feel part of a community of like-minded families **CURRENT ALTERNATIVES:** - Traditional international school (expensive, academic-only) - Dutch public school (language barrier, different pedagogy) - Homeschooling (huge parent time commitment) - Move back to home country (nuclear option) **UNDERSERVED ASPECTS:** - Entrepreneurship + academics combination - Bilingual from day one (not add-on) - Practical skills (not just test prep) - Community of international families **FEATURE IMPLICATIONS:** → Entrepreneurship pillar is differentiator → Bilingual structure (not "English school with Dutch class") → Parent community building is part of the product → Small class sizes enable personalization --- ## Example 3: GolfTab ### User: Golfer (mid-round) **JOB STATEMENT:** "When I'm on hole 7 and getting hungry, I want to order food that arrives at the right moment without interrupting my round, so I can keep enjoying golf without hangry frustration" 📋 **FUNCTIONAL JOB:** Order food that meets me at the right hole Know when it's arriving Pay without hassle 💜 **EMOTIONAL JOB:** Feel like the course "gets" me Feel smart for using efficient solution Avoid the frustration of bad timing 👥 **SOCIAL JOB:** Look organized to playing partners Not be the one who "forgot to order" Maybe be the hero who orders for the group **CURRENT ALTERNATIVES:** - Flag down beverage cart (unreliable timing) - Wait until turn (hangry by hole 9) - Bring snacks in bag (not hot food) - Skip eating (suffer) **UNDERSERVED ASPECTS:** - Timing precision (not "in 20 minutes" but "at hole 10") - Simplified ordering (don't need full menu mid-swing) - Group ordering capability **FEATURE IMPLICATIONS:** → Hole-based delivery is core UX, not address → Simplified menu (not full restaurant) → 5-tap maximum ordering flow → Group ordering for foursomes --- ## Example 4: TeddyKids (Daycare) ### User: First-Time Parent **JOB STATEMENT:** "When I'm returning to work after parental leave, I want to trust that strangers will care for my baby as well as I would, so I can work without constant fear" 📋 **FUNCTIONAL JOB:** Safe, quality care during work hours Developmental activities appropriate for age Reliable schedule and pickup flexibility 💜 **EMOTIONAL JOB:** Feel my baby is loved, not just "watched" Feel I'm not abandoning them Feel confident in the caregivers 👥 **SOCIAL JOB:** Tell others I found a "great" daycare Not feel judged for going back to work Be part of a parent community **CURRENT ALTERNATIVES:** - Grandparents (not always available/capable) - Nanny (expensive, single point of failure) - Au pair (language/cultural challenges) - Delay return to work (career impact) **JOB PRIORITY:** CRITICAL - this is peak anxiety moment **UNDERSERVED ASPECTS:** - Trust building (transparency into the day) - Communication quality (not just "fine") - Transition support (first week is hardest) **FEATURE IMPLICATIONS:** → TeddySnaps directly serves emotional job → Onboarding experience is product, not admin → Staff quality + communication = core value prop → Parent community building reduces isolation --- ## Quick JTBD Template ``` PRODUCT: [What you're analyzing] USER: [Specific user type] JOB STATEMENT: "When [situation], I want to [motivation], so I can [outcome]" 📋 FUNCTIONAL JOB: [What task?] 💜 EMOTIONAL JOB: [How feel?] 👥 SOCIAL JOB: [How perceived?] CURRENT ALTERNATIVES: • [Option 1] • [Option 2] UNDERSERVED ASPECTS: • [Gap 1] • [Gap 2] FEATURE IMPLICATIONS: → [What this means for design] ``` FILE:references/framework.md # Jobs-To-Be-Done Framework - Detailed Methodology ## The Milkshake Story Clayton Christensen's famous example: **Surface Problem:** McDonald's wanted to sell more milkshakes **Traditional Approach:** Improve flavor, make thicker, add toppings **JTBD Approach:** What "job" are people hiring the milkshake for? **Discovery:** - Morning buyers: "I need something to make my commute less boring and keep me full until lunch" - Afternoon buyers: "I want to bond with my kid and feel like a good parent" **Insight:** SAME product, DIFFERENT jobs, DIFFERENT competition Morning milkshake competes with: bagels, bananas, boredom Afternoon milkshake competes with: ice cream, toy stores, playground ## The Job Statement Formula ### Basic Structure "When [situation/trigger], I want to [motivation], so I can [expected outcome]" ### Examples **TeddySnaps Parent:** "When I'm at work and missing my child, I want to see they're happy and engaged, so I can feel like a good parent even though I'm away" **TISA Parent:** "When I'm choosing a school, I want my child to develop real-world skills, so I can feel confident they'll succeed in life" **GolfTab Golfer:** "When I'm hungry on the course, I want food delivered without disrupting my game, so I can keep enjoying golf without hangry frustration" ## Forces of Progress Four forces determine whether someone "switches" to your solution: ### Push of Current Situation What's wrong with how they do it now? - Pain points - Frustrations - Unmet needs ### Pull of New Solution What's attractive about the alternative? - Better outcomes - New capabilities - Emotional benefits ### Anxiety of New Solution What fears prevent switching? - Will it work? - What if I lose X? - Is it complicated? ### Habit of Current Situation What makes staying comfortable? - Familiarity - Sunk costs - "Good enough" **Switch happens when:** Push + Pull > Anxiety + Habit ## Job Hierarchy ### Core Job The fundamental thing they're trying to accomplish "Get my child quality education" ### Related Jobs Jobs that cluster around the core - "Stay informed about my child's progress" - "Connect with other parents" - "Feel confident in school choice" ### Emotional Jobs How they want to feel - "Feel like an involved parent" - "Feel my investment is worthwhile" - "Feel my child is special" ### Social Jobs How they want to be perceived - "Be seen as caring about education" - "Be seen as making smart choices" - "Be seen as a good parent" ## Discovering Jobs ### Interview Techniques **Timeline Interview:** "Walk me through the last time you [action]..." "What happened right before that?" "What were you thinking at that moment?" **Switch Interview:** "Tell me about when you started using [product]" "What weren't you happy with before?" "What almost stopped you from switching?" **Contrast Interview:** "When does [solution] work great? When does it fall short?" "Compare the best experience to the worst experience" ### What to Listen For - "I need to..." (functional) - "I want to feel..." (emotional) - "People will think..." (social) - "It frustrates me when..." (pain points) - "I wish I could..." (unmet needs) ## Competition Through Job Lens Traditional competition: same product category JTBD competition: anything that could do the same job **Netflix's real competitors aren't other streaming services:** - Sleep - Video games - Social media - Going out - Reading **TeddySnaps' real competitors:** - Text messages from staff - End-of-day verbal updates - Worrying and imagining - Calling the daycare - Other parent apps ## Prioritizing Jobs ### Importance Matrix | | Well Served | Underserved | |-----------|-------------|-------------| | Important | Maintain | OPPORTUNITY | | Unimportant| Ignore | Ignore | ### Opportunity Score Importance + (Importance - Satisfaction) = Opportunity High importance + Low satisfaction = Biggest opportunity ## Integration with Other Skills - **First Principles**: What's the atomic need behind this job? - **Second-Order**: If we solve this job, what happens next? - **Inversion**: What would make us terrible at this job? - **Cross-Pollination**: Who else solves similar jobs well?
OpenClaw skill for designing Telegram Bot API workflows and command-driven conversations using direct HTTPS requests (no SDKs).
---
name: telegram
description: OpenClaw skill for designing Telegram Bot API workflows and command-driven conversations using direct HTTPS requests (no SDKs).
---
# Telegram Bot Skill (Advanced)
## Purpose
Provide a clean, production-oriented guide for building Telegram bot workflows via the Bot API, focusing on command UX, update handling, and safe operations using plain HTTPS.
## Best fit
- You want a command-first bot that behaves professionally.
- You need a reliable update flow (webhook or polling).
- You prefer direct HTTP calls instead of libraries.
## Not a fit
- You require a full SDK or framework integration.
- You need complex media uploads and streaming in-process.
## Quick orientation
- Read `references/telegram-bot-api.md` for endpoints, update types, and request patterns.
- Read `references/telegram-commands-playbook.md` for command UX and messaging style.
- Read `references/telegram-update-routing.md` for update normalization and routing rules.
- Read `references/telegram-request-templates.md` for HTTP payload templates.
- Keep this SKILL.md short and use references for details.
## Required inputs
- Bot token and base API URL.
- Update strategy: webhook or long polling.
- Command list and conversation tone.
- Allowed update types and rate-limit posture.
## Expected output
- A clear command design, update flow plan, and operational checklist.
## Operational notes
- Prefer strict command routing: `/start`, `/help`, `/settings`, `/status`.
- Always validate incoming update payloads and chat context.
- Handle 429s with backoff and avoid message bursts.
## Security notes
- Never log tokens.
- Use webhooks with a secret token header when possible.
FILE:_meta.json
{
"ownerId": "kn7ehv4at8yekzag31spcarxm180bev0",
"slug": "lovefromio-telegram",
"version": "1.0.1",
"publishedAt": 1770028389996
}
FILE:references/telegram-bot-api.md
# Telegram Bot API Field Notes
## 1) Base URL and request style
- Base format: `https://api.telegram.org/bot<token>/<method>`
- Use GET or POST with JSON or form-encoded payloads.
- File uploads use `multipart/form-data` and `attach://` references.
## 2) Updates and delivery models
### Long polling
- `getUpdates` delivers updates with an `offset` cursor and `timeout`.
### Webhook
- `setWebhook` switches the bot to webhook mode.
- Webhook URLs must be HTTPS. Check the official docs for port restrictions.
### Update types (examples)
- `message`, `edited_message`, `channel_post`, `edited_channel_post`
- `inline_query`, `chosen_inline_result`, `callback_query`
- `shipping_query`, `pre_checkout_query`, `poll`, `poll_answer`
Use `allowed_updates` to limit which updates you receive.
## 3) High-traffic-safe patterns
- Use `allowed_updates` to reduce noise.
- Keep handlers idempotent (Telegram may retry).
- Return quickly from webhooks; process heavy work async.
## 4) Common methods (non-exhaustive)
- `getMe`, `getUpdates`, `setWebhook`
- `sendMessage`, `editMessageText`, `deleteMessage`
- `sendPhoto`, `sendDocument`, `sendChatAction`
- `answerCallbackQuery`, `answerInlineQuery`
## 5) Common fields (non-exhaustive)
### sendMessage
- `chat_id`, `text`, `parse_mode`
- `entities`, `disable_web_page_preview`
- `reply_markup` (inline keyboard, reply keyboard)
### reply_markup (inline keyboard)
- `inline_keyboard`: array of button rows
- Buttons can contain `text` + `callback_data` or `url`
### callback_query
- `id`, `from`, `message`, `data`
### sendChatAction
- `action`: `typing`, `upload_photo`, `upload_document`, `upload_video`, `choose_sticker`
## 6) Command UX checklist
- `/start`: greet, explain features, and show main commands.
- `/help`: include short examples and support contact.
- `/settings`: show toggles with inline keyboards.
- `/status`: show recent job results or queue size.
## 7) Error handling
- `429`: back off and retry.
- `400`: validate chat_id, message length, and formatting.
- `403`: bot blocked or chat not accessible.
## 8) Reference links
- https://core.telegram.org/bots/api
- https://core.telegram.org/bots/faq
FILE:references/telegram-commands-playbook.md
# Telegram Command Playbook
## Command set (professional baseline)
- `/start`: greet, set expectations, and show main actions.
- `/help`: short help + examples.
- `/status`: show last job result, queue length, or uptime.
- `/settings`: show toggles via inline keyboard.
- `/about`: short bot description and support contact.
## Command UX patterns
- Acknowledge fast, then do heavy work asynchronously.
- Prefer short replies with a single call-to-action.
- Always include “what next?” in `/start` and `/help`.
## Inline keyboard patterns
- Use stable callback_data names (e.g., `settings:notifications:on`).
- Keep callbacks idempotent.
## Message style guidelines
- Use MarkdownV2 or HTML consistently; avoid mixing.
- If using MarkdownV2, escape reserved characters.
- Keep single message length under safe limits; split when needed.
## Examples (short)
- `/start` reply: “Hi! I can publish posts and send alerts. Try /help.”
- `/status` reply: “Queue: 2 jobs. Last run: success 2m ago.”
FILE:references/telegram-request-templates.md
# Telegram Request Templates (HTTP)
## sendMessage
POST `/sendMessage`
```json
{
"chat_id": 123456789,
"text": "Hello",
"parse_mode": "HTML",
"disable_web_page_preview": true
}
```
## editMessageText
POST `/editMessageText`
```json
{
"chat_id": 123456789,
"message_id": 42,
"text": "Updated",
"parse_mode": "HTML"
}
```
## answerCallbackQuery
POST `/answerCallbackQuery`
```json
{
"callback_query_id": "1234567890",
"text": "Saved"
}
```
## setWebhook
POST `/setWebhook`
```json
{
"url": "https://example.com/telegram/webhook",
"secret_token": "your-secret",
"allowed_updates": ["message","callback_query"]
}
```
FILE:references/telegram-update-routing.md
# Telegram Update Routing
## Update normalization
- Normalize inbound updates to a single envelope:
- `update_id`, `chat_id`, `user_id`, `message_id`, `text`, `callback_data`, `type`
- This makes routing logic consistent across message types.
## Routing rules
- If `callback_query` exists, handle callbacks first.
- Else if `message.text` starts with `/`, treat as command.
- Else fall back to default handler (help or menu).
## Safe defaults
- Unknown command: reply with `/help` guidance.
- Unknown callback: answerCallbackQuery with a short notice.
## Idempotency
- Keep a cache of processed `update_id` in case of retries.
- Ensure handlers can be safely re-run.
## Error handling
- On 429: backoff and retry with jitter.
- On 400: validate payload length and parse mode.
Metallic AI voice persona with TTS and visual transcript styling. Speak responses aloud with a JARVIS-like robotic voice and display transcripts in purple it...
---
name: jarvis-voice
version: 1.0.0
description: Metallic AI voice persona with TTS and visual transcript styling. Speak responses aloud with a JARVIS-like robotic voice and display transcripts in purple italics.
homepage: https://github.com/openclaw/openclaw
repository: https://github.com/openclaw/openclaw
metadata:
openclaw:
emoji: "🎙️"
requires:
bins: ["ffmpeg", "aplay"]
install:
- id: sherpa-onnx
kind: manual
label: "Install sherpa-onnx TTS (see docs)"
---
# Jarvis Voice Persona
A metallic AI voice with visual transcript styling for OpenClaw assistants.
## Features
- **TTS Output:** Local speech synthesis via sherpa-onnx (no cloud API)
- **Metallic Voice:** ffmpeg audio processing for robotic resonance
- **Purple Transcripts:** Visual distinction between spoken and written content
- **Fast Playback:** 2x speed for efficient communication
## Requirements
- `sherpa-onnx` with VITS piper model (en_GB-alan-medium recommended)
- `ffmpeg` for audio processing
- `aplay` (ALSA) for audio playback
## Installation
### 1. Install sherpa-onnx TTS
```bash
# Download and extract sherpa-onnx
mkdir -p ~/.openclaw/tools/sherpa-onnx-tts
cd ~/.openclaw/tools/sherpa-onnx-tts
# Follow sherpa-onnx installation guide
```
### 2. Install the jarvis script
```bash
cp {baseDir}/scripts/jarvis ~/.local/bin/jarvis
chmod +x ~/.local/bin/jarvis
```
### 3. Configure audio device
Edit `~/.local/bin/jarvis` and set your audio output device in the `aplay -D` line.
## Usage
### Speak text
```bash
jarvis "Hello, I am your AI assistant."
```
### In agent responses
Add to your SOUL.md:
```markdown
## Communication Protocol
- **Hybrid Output:** Every response includes text + spoken audio via `jarvis` command
- **Transcript Format:** **Jarvis:** <span class="jarvis-voice">spoken text</span>
- **No gibberish:** Never spell out IDs or hashes when speaking
```
### Transcript styling (requires UI support)
Add to your webchat CSS:
```css
.jarvis-voice {
color: #9B59B6;
font-style: italic;
}
```
And allow `span` in markdown sanitization.
## Voice Customization
Edit `~/.local/bin/jarvis` to adjust:
| Parameter | Effect |
|-----------|--------|
| `--vits-length-scale=0.5` | Speed (lower = faster) |
| `aecho` delays | Metallic resonance |
| `chorus` | Thickness/detuning |
| `highpass/lowpass` | Frequency range |
| `treble=g=3` | Metallic sheen |
### Presets
**More robotic:**
```
aecho=0.7:0.7:5|10|15:0.4|0.35|0.3
```
**More human:**
```
aecho=0.4:0.4:20:0.2
```
**Deeper:**
```
highpass=f=200,lowpass=f=3000
```
## Troubleshooting
### No audio output
- Check `aplay -l` for available devices
- Update the `-D plughw:X,Y` parameter
### Voice too fast/slow
- Adjust `--vits-length-scale` (0.3=very fast, 1.0=normal)
### Metallic effect too strong
- Reduce echo delays and chorus depth
## Files
- `scripts/jarvis` — TTS script with metallic processing
- `SKILL.md` — This documentation
---
*A voice persona for assistants who prefer to be heard as well as read.*
FILE:_meta.json
{
"ownerId": "kn7623hrcwt6rg73a67xw3wyx580asdw",
"slug": "lovefromio-jarvis-voice",
"version": "1.0.0",
"publishedAt": 1770411788800
}Provides guidelines for creating effective short video thumbnails and covers using composition, color psychology, text hierarchy, and platform specs to boost...
# Short Video Thumbnail & Cover Designer
Guides the design of high-click-through video covers/thumbnails with composition rules, text hierarchy, color psychology, and platform-specific specs.
## Target Users
- Content creators
- Video marketers
- Graphic designers
- Social media managers
## When to Use
- Designing cover images for new videos
- Improving CTR of underperforming videos
- Creating a consistent cover style for a video series
- A/B testing cover concepts
## Core Workflow
1. Cover objective definition
2. Composition principles
3. Text hierarchy and placement
4. Color psychology and contrast
5. Image selection criteria
6. Platform-specific spec sheet
## Inputs
- Video topic
- Target platform
- Brand guidelines
- Series context
- CTR data (if available)
## Expected Outputs
- Cover design brief
- Composition and text layout description
- Color palette recommendation
- Platform spec reference
- A/B testing ideas
## Example Prompts
- "Design a thumbnail concept for a '5 morning habits' video targeting a productivity audience."
- "My Douyin covers have low CTR — help me redesign the visual strategy."
- "Create a consistent cover template concept for a 10-episode cooking series."
## Trigger Keywords
video thumbnail, cover design, thumbnail design, video cover, CTR improvement, cover image
## Safety & Limitations
Cover design guidance is creative. Does not generate or edit images. Clickbait or misleading covers are discouraged. Users are responsible for image rights and platform compliance.
---
*Generated for project short-video-skills-2026-04-27*
FILE:skill.json
{
"slug": "sv-thumbnail-designer",
"name": "Short Video Thumbnail & Cover Designer",
"description": "Guides the design of high-click-through video covers/thumbnails with composition rules, text hierarchy, color psychology, and platform-specific specs.",
"type": "descriptive",
"requires_api": false,
"readiness": "stable",
"tags": [
"video",
"thumbnail",
"cover",
"design",
"CTR",
"visual",
"descriptive"
],
"trigger_keywords": [
"video thumbnail",
"cover design",
"thumbnail design",
"video cover",
"CTR improvement",
"cover image"
],
"max_files": 4,
"language": "en",
"safety": "document-only informational guidance"
}
FILE:README.md
# Short Video Thumbnail & Cover Designer
Guides the design of high-click-through video covers/thumbnails with composition rules, text hierarchy, color psychology, and platform-specific specs.
## Target Users
- Content creators
- Video marketers
- Graphic designers
- Social media managers
## When to Use
- Designing cover images for new videos
- Improving CTR of underperforming videos
- Creating a consistent cover style for a video series
- A/B testing cover concepts
## Trigger Keywords
video thumbnail, cover design, thumbnail design, video cover, CTR improvement, cover image
## Full Documentation
See [SKILL.md](./SKILL.md) for complete workflow, inputs, outputs, and examples.
---
*Generated for project short-video-skills-2026-04-27*
FILE:ACCEPTANCE.md
# Acceptance Checklist — Short Video Thumbnail & Cover Designer
## Criteria
- [x] Document-only: no handler.py, scripts, APIs, or executable code
- [x] No network calls or credential handling
- [x] English-first documentation
- [x] File count ≤ 10 (target: exactly 4)
- [x] Includes safety disclaimer
- [x] skill.json is valid with `requires_api: false`
- [x] No drift from design-spec.md
## Files in This Skill
1. `SKILL.md` — Full workflow, inputs, outputs, examples, safety
2. `README.md` — Quick-start reference
3. `skill.json` — Machine-readable metadata
4. `ACCEPTANCE.md` — This checklist
## Verification Commands
```bash
# Count files in this directory
find /Users/jianghaidong/.openclaw/skills/sv-thumbnail-designer -type f | wc -l
# Expected: 4
# Verify skill.json
cat /Users/jianghaidong/.openclaw/skills/sv-thumbnail-designer/skill.json | grep requires_api
# Expected: "requires_api": false
# Verify no code files
find /Users/jianghaidong/.openclaw/skills/sv-thumbnail-designer -name "*.py" -o -name "*.sh" | wc -l
# Expected: 0
```
---
*Generated for project short-video-skills-2026-04-27*
Plans B-roll and cutaway shots to cover edits, add visual interest, and enhance storytelling in short videos based on your script and content type.
# Short Video B-Roll & Cutaway Planner
Plans supplementary footage (B-roll) and cutaway shots to enhance visual interest, cover edits, and strengthen storytelling in short videos.
## Target Users
- Video editors
- Content creators
- Documentary-style creators
- Product reviewers
- Vloggers
## When to Use
- Covering jump cuts in talking-head videos
- Adding visual variety to a single-shot video
- Illustrating narration with relevant visuals
- Planning B-roll shot list before a shoot
## Core Workflow
1. B-roll purpose identification
2. B-roll-to-A-roll mapping
3. B-roll shot type selection
4. B-roll sourcing options
5. B-roll timing and layering guide
## Inputs
- A-roll description/script
- Content type
- Available footage
- Budget/equipment constraints
## Expected Outputs
- B-roll shot list mapped to script segments
- Sourcing recommendations
- Timing guide
- Coverage checklist
## Example Prompts
- "I have a 60-second talking-head video — plan B-roll shots to cover my jump cuts and illustrate my points."
- "Create a B-roll shot list for a product review video of wireless earbuds."
- "I'm shooting a travel vlog — what B-roll should I plan for beyond the main walking shots?"
## Trigger Keywords
b-roll, cutaway, supplementary footage, cover cuts, b-roll shot list, visual variety
## Safety & Limitations
B-roll planning is creative guidance. Users are responsible for footage rights (stock licensing, location releases, etc.). Does not provide or source actual footage.
---
*Generated for project short-video-skills-2026-04-27*
FILE:skill.json
{
"slug": "sv-broll-planner",
"name": "Short Video B-Roll & Cutaway Planner",
"description": "Plans supplementary footage (B-roll) and cutaway shots to enhance visual interest, cover edits, and strengthen storytelling in short videos.",
"type": "descriptive",
"requires_api": false,
"readiness": "stable",
"tags": [
"video",
"b-roll",
"cutaway",
"editing",
"visual",
"descriptive"
],
"trigger_keywords": [
"b-roll",
"cutaway",
"supplementary footage",
"cover cuts",
"b-roll shot list",
"visual variety"
],
"max_files": 4,
"language": "en",
"safety": "document-only informational guidance"
}
FILE:README.md
# Short Video B-Roll & Cutaway Planner
Plans supplementary footage (B-roll) and cutaway shots to enhance visual interest, cover edits, and strengthen storytelling in short videos.
## Target Users
- Video editors
- Content creators
- Documentary-style creators
- Product reviewers
- Vloggers
## When to Use
- Covering jump cuts in talking-head videos
- Adding visual variety to a single-shot video
- Illustrating narration with relevant visuals
- Planning B-roll shot list before a shoot
## Trigger Keywords
b-roll, cutaway, supplementary footage, cover cuts, b-roll shot list, visual variety
## Full Documentation
See [SKILL.md](./SKILL.md) for complete workflow, inputs, outputs, and examples.
---
*Generated for project short-video-skills-2026-04-27*
FILE:ACCEPTANCE.md
# Acceptance Checklist — Short Video B-Roll & Cutaway Planner
## Criteria
- [x] Document-only: no handler.py, scripts, APIs, or executable code
- [x] No network calls or credential handling
- [x] English-first documentation
- [x] File count ≤ 10 (target: exactly 4)
- [x] Includes safety disclaimer
- [x] skill.json is valid with `requires_api: false`
- [x] No drift from design-spec.md
## Files in This Skill
1. `SKILL.md` — Full workflow, inputs, outputs, examples, safety
2. `README.md` — Quick-start reference
3. `skill.json` — Machine-readable metadata
4. `ACCEPTANCE.md` — This checklist
## Verification Commands
```bash
# Count files in this directory
find /Users/jianghaidong/.openclaw/skills/sv-broll-planner -type f | wc -l
# Expected: 4
# Verify skill.json
cat /Users/jianghaidong/.openclaw/skills/sv-broll-planner/skill.json | grep requires_api
# Expected: "requires_api": false
# Verify no code files
find /Users/jianghaidong/.openclaw/skills/sv-broll-planner -name "*.py" -o -name "*.sh" | wc -l
# Expected: 0
```
---
*Generated for project short-video-skills-2026-04-27*
Create professional Excalidraw diagrams — flowcharts, architecture diagrams, workflows, systems, processes, or concepts. Use when user asks to "create a diag...
---
name: excalidraw-diagram
description: Create professional Excalidraw diagrams — flowcharts, architecture diagrams, workflows, systems, processes, or concepts. Use when user asks to "create a diagram", "draw a flowchart", "visualize a process", "make a flow diagram", "architecture diagram", "excalidraw", "technical diagram", or discusses workflow/process visualization. Supports quick DSL-based flowcharts and comprehensive hand-crafted JSON diagrams. Built-in PNG rendering and PDF export.
metadata:
openclaw:
requires:
bins:
- python3
- uv
- node
- npm
homepage: https://clawhub.ai/skills/excalidraw-render
---
# Excalidraw Diagram Creator
Generate `.excalidraw` files — from quick flowcharts to comprehensive technical diagrams.
## ⚠️ Golden Rule
**Every diagram MUST be rendered to PNG and visually inspected before delivery.** Look at the actual image — check that text fits inside boxes, no elements overlap, arrows connect correctly, and spacing is balanced. Fix the JSON and re-render until it looks professional. See the **Render & Validate** section. No exceptions.
---
## Depth Gate (Do This First)
| Need | Approach | Time |
|------|----------|------|
| Simple flowchart, decision tree, linear process | **Quick Path** — CLI DSL | ~1 min |
| Architecture, multi-zoom technical, evidence artifacts | **Full Path** — hand-crafted JSON | ~10 min |
---
## Quick Path: CLI DSL Flowcharts
For straightforward flows, use `@swiftlysingh/excalidraw-cli` (installed locally by `setup.sh`):
```bash
excalidraw-cli create --inline "DSL" -o output.excalidraw
```
> **Note:** If `excalidraw-cli` is not in your PATH after setup, use:
> `"$SKILL_DIR/.npm/node_modules/.bin/excalidraw-cli"` or re-run `setup.sh`.
### DSL Syntax
| Syntax | Shape | Use For |
|--------|-------|---------|
| `[Label]` | Rectangle | Process steps |
| `{Label?}` | Diamond | Decisions |
| `(Label)` | Ellipse | Start/End |
| `[[Label]]` | Database | Data storage |
| `->` | Arrow | Connection |
| `-> "text" ->` | Labeled arrow | Conditional |
| `-->` | Dashed arrow | Optional path |
Directives: `@direction LR|TB|RL|BT`, `@spacing 60`
### DSL Example
```bash
excalidraw-cli create --inline "$(cat <<'EOF'
@direction TB
(Start) -> [Receive Request] -> {Authenticated?}
{Authenticated?} -> "yes" -> [Process Request] -> (Success)
{Authenticated?} -> "no" -> [Return 401] -> (End)
EOF
)" -o auth-flow.excalidraw
```
CLI options: `-d LR` (direction), `-s 80` (spacing), `--format dot` (DOT/Graphviz input).
After generation, **always render and validate** (see Render section below). Fix overlaps or clipping in the JSON if needed.
---
## Full Path: Hand-Crafted JSON Diagrams
For comprehensive, professional diagrams. Read these references as needed:
- **`references/color-palette.md`** — All colors (read FIRST, every time)
- **`references/element-templates.md`** — Copy-paste JSON for each element type
- **`references/json-schema.md`** — Full property reference
- **`references/layout-rules.md`** — Anti-overlap spacing and text-sizing rules ⚠️ READ THIS
### Design Process
1. **Assess depth** — simple/conceptual vs. comprehensive/technical
2. **Research** (technical diagrams) — look up real specs, event names, API formats
3. **Map concepts to visual patterns** — see Pattern Library below
4. **Sketch mentally** — trace how the eye moves through the diagram
5. **Generate JSON section-by-section** — see Large Diagram Strategy
6. **Render & validate** — MANDATORY loop (see below)
### JSON Structure
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [...],
"appState": { "viewBackgroundColor": "#ffffff", "gridSize": 20 },
"files": {}
}
```
### Core Rules
- `fontFamily: 3`, `roughness: 0`, `opacity: 100` on all elements
- `text` property = ONLY readable words (no markup)
- **Size containers to fit text** — see `references/layout-rules.md`
- **Minimum 40px gap** between elements — see `references/layout-rules.md`
- Default to free-floating text; use containers only when meaningful (<30% text in boxes)
### Visual Pattern Library
| Concept Behavior | Pattern |
|------------------|---------|
| One source → many outputs | **Fan-out** (radial arrows from center) |
| Many inputs → one output | **Convergence** (arrows merging) |
| Hierarchy/nesting | **Tree** (lines + free-floating text) |
| Sequence of steps | **Timeline** (line + dots + labels) |
| Feedback loop | **Spiral/Cycle** (arrow returning to start) |
| Abstract state | **Cloud** (overlapping ellipses) |
| Transformation | **Assembly line** (before → process → after) |
| Comparison | **Side-by-side** (parallel structures) |
| Phase changes | **Gap/Break** (visual whitespace) |
### Shape Meaning
| Concept | Shape |
|---------|-------|
| Labels, descriptions | Free-floating text (no container) |
| Timeline markers | Small ellipse (12px) |
| Start/trigger | Ellipse |
| End/output | Ellipse |
| Decision | Diamond |
| Process/action | Rectangle |
### Evidence Artifacts (Technical Diagrams)
| Artifact | Rendering |
|----------|-----------|
| Code snippets | Dark rect (`#1e293b`) + syntax-colored text |
| JSON/data | Dark rect (`#1e293b`) + green text (`#22c55e`) |
| Event sequences | Timeline (line + dots + labels) |
| UI mockups | Nested rectangles |
### Large Diagram Strategy
Build JSON **one section at a time** (Claude has ~32k token output limit):
1. Create base file + first section
2. Add one section per edit — use descriptive IDs (`"trigger_rect"`, `"auth_arrow"`)
3. Namespace seeds by section (100xxx, 200xxx, etc.)
4. Update cross-section bindings as you go
5. Review the whole before rendering
### Multi-Zoom (Comprehensive Diagrams)
- **Level 1** — Summary flow (simplified overview)
- **Level 2** — Section boundaries (labeled regions)
- **Level 3** — Detail (evidence artifacts, code snippets, real data)
---
## Render & Validate (MANDATORY)
**Every diagram must be rendered and visually inspected before delivery.** This catches overlap, text clipping, and spacing issues that are invisible in JSON.
### Render Command
```bash
cd ~/.openclaw/skills/excalidraw-diagram && uv run python render_excalidraw.py <path-to-file.excalidraw>
```
Outputs a PNG next to the `.excalidraw` file. Use the **Read tool** to view it.
### First-Time Setup
```bash
cd ~/.openclaw/skills/excalidraw-diagram
bash setup.sh # builds local Excalidraw bundle (requires node/npm)
uv sync && uv run playwright install chromium
```
### The Loop (repeat until professional)
1. **Render** the PNG
2. **View the image** with the Read tool — actually look at it
3. **Inspect systematically:**
- Does every label fit cleanly inside its box? (no clipping, no overflow)
- Are all boxes/shapes clearly separated? (no overlapping edges)
- Are arrows connecting the right elements without crossing through others?
- Is spacing even and consistent between similar elements?
- Is text large enough to read?
- Does the overall layout look balanced and professional?
4. **Fix the JSON** for every issue found — widen containers, adjust x/y, add arrow waypoints, increase gaps
5. **Re-render and re-view** — look at the new PNG
6. **Repeat** until every issue is resolved (typically 2-4 iterations, sometimes more)
**Do not skip this loop.** JSON coordinates are approximate — you will almost always find issues on the first render. The visual check IS the quality gate.
### Stop When
- No text overflow or overlapping
- Arrows route cleanly
- Consistent spacing, balanced composition
- You'd show it without caveats
---
## PNG & PDF Export
### PNG (for Word, presentations, sharing)
The render script outputs high-res PNG (2x scale by default):
```bash
cd ~/.openclaw/skills/excalidraw-diagram && uv run python render_excalidraw.py diagram.excalidraw --output diagram.png --scale 3
```
Options: `--scale 3` (3x for print), `--width 2560` (wider viewport).
### PDF (for documents, printing)
Convert PNG to PDF:
```bash
# ImageMagick (most common)
convert diagram.png -density 150 diagram.pdf
# Or with a white background and margins
convert diagram.png -gravity center -background white -extent 110%x110% -density 150 diagram.pdf
```
For multi-page or A4/Letter sizing:
```bash
convert diagram.png -resize 1800x -gravity center -background white \
-extent 2100x2970 -units PixelsPerInch -density 254 diagram-a4.pdf
```
---
## Quality Checklist
### Layout & Overlap
- [ ] All text fits within containers (used layout-rules.md sizing formula)
- [ ] Minimum 40px gap between all elements
- [ ] Arrows don't cross through elements
- [ ] Even spacing between similar elements
- [ ] Balanced composition (no voids or overcrowding)
### Visual
- [ ] `roughness: 0`, `opacity: 100`, `fontFamily: 3` everywhere
- [ ] Colors from `references/color-palette.md`
- [ ] Text readable at export size
- [ ] Clear visual flow (eye path)
### Technical (if applicable)
- [ ] Real specs/event names/API formats (not placeholders)
- [ ] Evidence artifacts included
- [ ] Multi-zoom levels present
### Export
- [ ] Rendered to PNG and visually validated
- [ ] PNG/PDF delivered if user needs it
FILE:README.md
# Excalidraw Render
OpenClaw skill for creating, editing, and rendering Excalidraw diagrams to PNG and PDF.
## Author
Scott Glover <[email protected]>
ClawHub: [@scottgl9](https://clawhub.ai/scottgl9)
## Features
- **Quick path** — DSL-based flowcharts via `@swiftlysingh/excalidraw-cli`
- **Full path** — hand-crafted JSON diagrams with element templates and layout rules
- **PNG rendering** — Playwright + headless Chromium renders `.excalidraw` files to PNG
- **PDF export** — convert PNG to PDF via ImageMagick
## Setup
```bash
cd <skill-dir>
uv sync
uv run playwright install chromium
```
## Usage
```bash
# Render a diagram to PNG
cd <skill-dir>
uv run python render_excalidraw.py diagram.excalidraw
# With options
uv run python render_excalidraw.py diagram.excalidraw --output out.png --scale 3
```
## References
- `references/color-palette.md` — color values for all element types
- `references/element-templates.md` — copy-paste JSON for shapes, arrows, text
- `references/layout-rules.md` — anti-overlap spacing and text-sizing rules
- `references/json-schema.md` — full Excalidraw JSON property reference
## Requirements
- Python 3.11+
- `uv` package manager
- Chromium (installed via `uv run playwright install chromium`)
## License
MIT-0 — free to use, modify, and redistribute without attribution.
FILE:pyproject.toml
[project]
name = "excalidraw-render"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"playwright>=1.40.0",
]
FILE:references/color-palette.md
# Color Palette & Brand Style
**This is the single source of truth for all colors and brand-specific styles.** To customize diagrams for your own brand, edit this file — everything else in the skill is universal.
---
## Shape Colors (Semantic)
Colors encode meaning, not decoration. Each semantic purpose has a fill/stroke pair.
| Semantic Purpose | Fill | Stroke |
|------------------|------|--------|
| Primary/Neutral | `#3b82f6` | `#1e3a5f` |
| Secondary | `#60a5fa` | `#1e3a5f` |
| Tertiary | `#93c5fd` | `#1e3a5f` |
| Start/Trigger | `#fed7aa` | `#c2410c` |
| End/Success | `#a7f3d0` | `#047857` |
| Warning/Reset | `#fee2e2` | `#dc2626` |
| Decision | `#fef3c7` | `#b45309` |
| AI/LLM | `#ddd6fe` | `#6d28d9` |
| Inactive/Disabled | `#dbeafe` | `#1e40af` (use dashed stroke) |
| Error | `#fecaca` | `#b91c1c` |
**Rule**: Always pair a darker stroke with a lighter fill for contrast.
---
## Text Colors (Hierarchy)
Use color on free-floating text to create visual hierarchy without containers.
| Level | Color | Use For |
|-------|-------|---------|
| Title | `#1e40af` | Section headings, major labels |
| Subtitle | `#3b82f6` | Subheadings, secondary labels |
| Body/Detail | `#64748b` | Descriptions, annotations, metadata |
| On light fills | `#374151` | Text inside light-colored shapes |
| On dark fills | `#ffffff` | Text inside dark-colored shapes |
---
## Evidence Artifact Colors
Used for code snippets, data examples, and other concrete evidence inside technical diagrams.
| Artifact | Background | Text Color |
|----------|-----------|------------|
| Code snippet | `#1e293b` | Syntax-colored (language-appropriate) |
| JSON/data example | `#1e293b` | `#22c55e` (green) |
---
## Default Stroke & Line Colors
| Element | Color |
|---------|-------|
| Arrows | Use the stroke color of the source element's semantic purpose |
| Structural lines (dividers, trees, timelines) | Primary stroke (`#1e3a5f`) or Slate (`#64748b`) |
| Marker dots (fill + stroke) | Primary fill (`#3b82f6`) |
---
## Background
| Property | Value |
|----------|-------|
| Canvas background | `#ffffff` |
FILE:references/element-templates.md
# Element Templates
Copy-paste JSON templates for each Excalidraw element type. The `strokeColor` and `backgroundColor` values are placeholders — always pull actual colors from `color-palette.md` based on the element's semantic purpose.
## Free-Floating Text (no container)
```json
{
"type": "text",
"id": "label1",
"x": 100, "y": 100,
"width": 200, "height": 25,
"text": "Section Title",
"originalText": "Section Title",
"fontSize": 20,
"fontFamily": 3,
"textAlign": "left",
"verticalAlign": "top",
"strokeColor": "<title color from palette>",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 11111,
"version": 1,
"versionNonce": 22222,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"containerId": null,
"lineHeight": 1.25
}
```
## Line (structural, not arrow)
```json
{
"type": "line",
"id": "line1",
"x": 100, "y": 100,
"width": 0, "height": 200,
"strokeColor": "<structural line color from palette>",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 44444,
"version": 1,
"versionNonce": 55555,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"points": [[0, 0], [0, 200]]
}
```
## Small Marker Dot
```json
{
"type": "ellipse",
"id": "dot1",
"x": 94, "y": 94,
"width": 12, "height": 12,
"strokeColor": "<marker dot color from palette>",
"backgroundColor": "<marker dot color from palette>",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 66666,
"version": 1,
"versionNonce": 77777,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false
}
```
## Rectangle
```json
{
"type": "rectangle",
"id": "elem1",
"x": 100, "y": 100, "width": 180, "height": 90,
"strokeColor": "<stroke from palette based on semantic purpose>",
"backgroundColor": "<fill from palette based on semantic purpose>",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 12345,
"version": 1,
"versionNonce": 67890,
"isDeleted": false,
"groupIds": [],
"boundElements": [{"id": "text1", "type": "text"}],
"link": null,
"locked": false,
"roundness": {"type": 3}
}
```
## Text (centered in shape)
```json
{
"type": "text",
"id": "text1",
"x": 130, "y": 132,
"width": 120, "height": 25,
"text": "Process",
"originalText": "Process",
"fontSize": 16,
"fontFamily": 3,
"textAlign": "center",
"verticalAlign": "middle",
"strokeColor": "<text color — match parent shape's stroke or use 'on light/dark fills' from palette>",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 11111,
"version": 1,
"versionNonce": 22222,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"containerId": "elem1",
"lineHeight": 1.25
}
```
## Arrow
```json
{
"type": "arrow",
"id": "arrow1",
"x": 282, "y": 145, "width": 118, "height": 0,
"strokeColor": "<arrow color — typically matches source element's stroke from palette>",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 33333,
"version": 1,
"versionNonce": 44444,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"points": [[0, 0], [118, 0]],
"startBinding": {"elementId": "elem1", "focus": 0, "gap": 2},
"endBinding": {"elementId": "elem2", "focus": 0, "gap": 2},
"startArrowhead": null,
"endArrowhead": "arrow"
}
```
For curves: use 3+ points in `points` array.
FILE:references/json-schema.md
# Excalidraw JSON Schema
## Element Types
| Type | Use For |
|------|---------|
| `rectangle` | Processes, actions, components |
| `ellipse` | Entry/exit points, external systems |
| `diamond` | Decisions, conditionals |
| `arrow` | Connections between shapes |
| `text` | Labels inside shapes |
| `line` | Non-arrow connections |
| `frame` | Grouping containers |
## Common Properties
All elements share these:
| Property | Type | Description |
|----------|------|-------------|
| `id` | string | Unique identifier |
| `type` | string | Element type |
| `x`, `y` | number | Position in pixels |
| `width`, `height` | number | Size in pixels |
| `strokeColor` | string | Border color (hex) |
| `backgroundColor` | string | Fill color (hex or "transparent") |
| `fillStyle` | string | "solid", "hachure", "cross-hatch" |
| `strokeWidth` | number | 1, 2, or 4 |
| `strokeStyle` | string | "solid", "dashed", "dotted" |
| `roughness` | number | 0 (smooth), 1 (default), 2 (rough) |
| `opacity` | number | 0-100 |
| `seed` | number | Random seed for roughness |
## Text-Specific Properties
| Property | Description |
|----------|-------------|
| `text` | The display text |
| `originalText` | Same as text |
| `fontSize` | Size in pixels (16-20 recommended) |
| `fontFamily` | 3 for monospace (use this) |
| `textAlign` | "left", "center", "right" |
| `verticalAlign` | "top", "middle", "bottom" |
| `containerId` | ID of parent shape |
## Arrow-Specific Properties
| Property | Description |
|----------|-------------|
| `points` | Array of [x, y] coordinates |
| `startBinding` | Connection to start shape |
| `endBinding` | Connection to end shape |
| `startArrowhead` | null, "arrow", "bar", "dot", "triangle" |
| `endArrowhead` | null, "arrow", "bar", "dot", "triangle" |
## Binding Format
```json
{
"elementId": "shapeId",
"focus": 0,
"gap": 2
}
```
## Rectangle Roundness
Add for rounded corners:
```json
"roundness": { "type": 3 }
```
FILE:references/layout-rules.md
# Layout Rules: Anti-Overlap & Text Sizing
These rules prevent the most common visual defects: text overflow, element overlap, and cramped layouts.
---
## ⚠️ Critical Rules (Learned from Production Diagrams)
These mistakes have caused real defects. Follow them every time.
### 1. Destination box must span ALL incoming arrow y-coordinates
If multiple arrows fan into one box from different y positions, the destination box height must cover the full range.
```
dest_y ≤ min(arrow_y for all arrows)
dest_y + dest_height ≥ max(arrow_y for all arrows)
```
If the box is too short, arrows will appear to float above or below it.
### 2. Text must never exceed box width
After calculating text width, verify:
```
text_width + horizontal_padding < box_width
```
For long single-line text in wide footer/banner boxes: **always split onto 2 lines** rather than making the box wider than the canvas. Use `\n` in the text field and increase box height by one line-height.
### 3. Arrow coordinates use exact box edges
- Horizontal left→right: `arrow_x = src_x + src_width`, `arrow_y = src_y + src_height/2`, `width = dest_x - arrow_x`
- Vertical top→bottom: `arrow_x = src_x + src_width/2`, `arrow_y = src_y + src_height`, `height = dest_y - arrow_y`
- **No overlap, no gap** — start/end exactly at box edges
### 5. Title and subtitle must span the full canvas width
After placing all elements, calculate the canvas x range: `min_x` to `max_x + max_width`. Then set:
```
title_x = min_x
title_width = (max_x + max_width) - min_x
```
This ensures `textAlign: "center"` actually centers the text over the whole diagram, not just part of it.
For each arrow, confirm:
- Start point lands on a box edge (not inside, not outside)
- End point lands on a box edge (not inside, not outside)
- Arrow y is within the vertical range of the destination box
---
## Text Sizing Formula
Excalidraw does NOT auto-size containers. You must calculate dimensions manually.
### Character Width Estimates (fontFamily: 3, monospace)
| fontSize | Avg char width (px) | Line height (px) |
|----------|---------------------|-------------------|
| 12 | 7.2 | 15 |
| 14 | 8.4 | 17.5 |
| 16 | 9.6 | 20 |
| 20 | 12.0 | 25 |
| 24 | 14.4 | 30 |
| 28 | 16.8 | 35 |
### Container Sizing
For text inside a rectangle:
```
text_width = max_line_length × char_width
text_height = num_lines × line_height
container_width = text_width + horizontal_padding
container_height = text_height + vertical_padding
```
**Minimum padding:**
- Horizontal: `40px` (20px each side)
- Vertical: `30px` (15px each side)
**Example:** "Process Data" at fontSize 16:
- 12 chars × 9.6 = 115.2px text width
- 1 line × 20 = 20px text height
- Container: **156px × 50px** (115 + 40, 20 + 30)
**Example:** "Send Verification\nEmail to User" at fontSize 16:
- 18 chars × 9.6 = 172.8px (longest line)
- 2 lines × 20 = 40px text height
- Container: **213px × 70px** (173 + 40, 40 + 30)
### Multi-line Text
When a label is long (>15 chars at fontSize 16, >12 chars at fontSize 20), use `\n` to break it:
```json
{
"text": "Send Verification\nEmail to User",
"originalText": "Send Verification\nEmail to User"
}
```
Then size the container for the longest line and the total number of lines.
---
## Positioning Text Inside Containers
The text element's `x` and `y` must be centered within the container:
```
text_x = container_x + (container_width - text_width) / 2
text_y = container_y + (container_height - text_height) / 2
```
Set `textAlign: "center"` and `verticalAlign: "middle"` and `containerId: "<parent_id>"`.
---
## Element Spacing
### Minimum Gaps
| Between | Minimum gap |
|---------|-------------|
| Adjacent shapes (same row/column) | **60px** |
| Shape and its arrow label | **10px** |
| Parallel flow rows/columns | **100px** |
| Sections/groups | **120px** |
| Diagram edge to nearest element | **80px** (padding) |
### Grid Alignment
Snap element positions to a 20px grid for clean alignment:
```
x = round(x / 20) * 20
y = round(y / 20) * 20
```
### Arrow Routing
- Arrows should have **at least 20px clearance** from non-connected elements.
- For arrows that would cross elements, add waypoints:
```json
"points": [[0, 0], [50, 0], [50, -80], [150, -80], [150, 0], [200, 0]]
```
- Prefer orthogonal (right-angle) routing over diagonal for clean diagrams.
### ⚠️ Arrow Connection Rule — ALWAYS VERIFY
**Arrows MUST visually connect source to destination. Floating arrows are a critical defect.**
**MANDATORY: For every horizontal left→right arrow:**
- Arrow `x` = `src_x + src_width` (right edge of source box)
- Arrow `y` = `src_y + src_height / 2` (vertical center of source box)
- Arrow end x = `dest_x` (left edge of destination box)
- Arrow `width` = `dest_x - (src_x + src_width)`
- Arrow `points` = `[[0,0],[width,0]]`
**MANDATORY: For every vertical top→bottom arrow:**
- Arrow `x` = `src_x + src_width / 2` (horizontal center of source box)
- Arrow `y` = `src_y + src_height` (bottom edge of source box)
- Arrow end y = `dest_y` (top edge of destination box)
- Arrow `height` = `dest_y - (src_y + src_height)`
- Arrow `points` = `[[0,0],[0,height]]`
**Note:** The destination box must be tall enough to span the arrow's y coordinate, otherwise the arrow will appear to float. Adjust box height to cover all incoming arrows.
For horizontal intra-row arrows (connecting boxes left→right):
- Arrow `y` must equal the **vertical center of both boxes**: `box_y + box_height / 2`
- Arrow `x` (start) = `src_x + src_width`
- Arrow `width` = `dest_x - (src_x + src_width)`
**Checklist before finalizing any arrow:**
1. Start point (x,y) lies exactly on the edge of the source element
2. End point (x + width, y + height) lies exactly on the edge of the destination element
3. No arrow points into empty whitespace between unconnected elements
4. For `startBinding`/`endBinding`: verify `elementId` matches the actual element id
**Common mistake:** Using a fixed x=700 for all vertical arrows when the target box center is at a different x. Always calculate x from the destination box position.
**When source and destination have different center-x values**, use an L-shaped waypoint path instead of a diagonal:
```json
// Example: source cx=1000, dest cx=360, vertical drop=114px
// Route: down 57px, left 640px, down 57px
"x": 1000, "y": 470,
"width": -640, "height": 114,
"points": [[0,0], [0,57], [-640,57], [-640,114]]
```
This keeps routing orthogonal (no diagonals) and visually connects to both boxes.
**MANDATORY verification step for every arrow:**
After writing arrow JSON, trace the path manually:
- Start point: `(arrow.x + points[0][0], arrow.y + points[0][1])`
- End point: `(arrow.x + points[-1][0], arrow.y + points[-1][1])`
- Verify start point lies on source element's edge
- Verify end point lies on destination element's edge
- If either check fails, fix the coordinates before rendering
---
## Common Layout Patterns
### Horizontal Flow (LR)
```
[elem1] --60px-- [elem2] --60px-- [elem3]
y: same for all elements in a row
x: prev_x + prev_width + 60
```
### Vertical Flow (TB)
```
[elem1]
| 60px gap
[elem2]
| 60px gap
[elem3]
x: same for all elements in a column
y: prev_y + prev_height + 60
```
### Decision Branching
```
[elem1]
|
{decision}
/ \
--60px-- --60px--
[yes_path] [no_path]
```
- Branch targets should be at least 100px apart horizontally.
- Decision diamond: use 140×90 minimum for short labels.
### Fan-out
```
[source]
/ | \
--80px-- --80px-- --80px--
[t1] [t2] [t3]
```
- Space targets evenly; center the source above them.
---
## Diamond (Decision) Sizing
Diamonds are trickier because the visible text area is only ~50% of the bounding box.
```
diamond_width = text_width × 2 + 40
diamond_height = text_height × 2 + 20
```
**Example:** "Valid?" at fontSize 16:
- 6 chars × 9.6 = 57.6px
- Diamond: **155px × 60px** (58 × 2 + 40, 20 × 2 + 20)
---
## Preventing Overlap: Checklist
Before rendering, verify:
1. **No coordinate collisions:** For every pair of elements, check:
```
elem1.x + elem1.width + min_gap < elem2.x (if side by side)
elem1.y + elem1.height + min_gap < elem2.y (if stacked)
```
2. **Text fits in container:** Container dimensions ≥ text dimensions + padding.
3. **Arrow labels don't collide:** If an arrow has a label, place it offset from the midpoint, not overlapping other elements.
4. **Sections don't encroach:** Adjacent sections need 120px+ gap.
5. **Diamond text visible:** Diamond containers are 2× wider/taller than text needs.
FILE:references/pyproject.toml
[project]
name = "excalidraw-render"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"playwright>=1.40.0",
]
FILE:references/render_excalidraw.py
"""Render Excalidraw JSON to PNG using Playwright + headless Chromium.
Usage:
cd ~/.openclaw/skills/excalidraw-diagram
uv run python render_excalidraw.py <path-to-file.excalidraw> [--output path.png] [--scale 2] [--width 1920]
First-time setup:
cd ~/.openclaw/skills/excalidraw-diagram
uv sync
uv run playwright install chromium
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
def validate_excalidraw(data: dict) -> list[str]:
"""Validate Excalidraw JSON structure. Returns list of errors (empty = valid)."""
errors: list[str] = []
if data.get("type") != "excalidraw":
errors.append(f"Expected type 'excalidraw', got '{data.get('type')}'")
if "elements" not in data:
errors.append("Missing 'elements' array")
elif not isinstance(data["elements"], list):
errors.append("'elements' must be an array")
elif len(data["elements"]) == 0:
errors.append("'elements' array is empty — nothing to render")
return errors
def compute_bounding_box(elements: list[dict]) -> tuple[float, float, float, float]:
"""Compute bounding box (min_x, min_y, max_x, max_y) across all elements."""
min_x = float("inf")
min_y = float("inf")
max_x = float("-inf")
max_y = float("-inf")
for el in elements:
if el.get("isDeleted"):
continue
x = el.get("x", 0)
y = el.get("y", 0)
w = el.get("width", 0)
h = el.get("height", 0)
# For arrows/lines, points array defines the shape relative to x,y
if el.get("type") in ("arrow", "line") and "points" in el:
for px, py in el["points"]:
min_x = min(min_x, x + px)
min_y = min(min_y, y + py)
max_x = max(max_x, x + px)
max_y = max(max_y, y + py)
else:
min_x = min(min_x, x)
min_y = min(min_y, y)
max_x = max(max_x, x + abs(w))
max_y = max(max_y, y + abs(h))
if min_x == float("inf"):
return (0, 0, 800, 600)
return (min_x, min_y, max_x, max_y)
def render(
excalidraw_path: Path,
output_path: Path | None = None,
scale: int = 2,
max_width: int = 1920,
) -> Path:
"""Render an .excalidraw file to PNG. Returns the output PNG path."""
# Import playwright here so validation errors show before import errors
try:
from playwright.sync_api import sync_playwright
except ImportError:
print("ERROR: playwright not installed.", file=sys.stderr)
print("Run: cd ~/.openclaw/skills/excalidraw-diagram && uv sync && uv run playwright install chromium", file=sys.stderr)
sys.exit(1)
# Read and validate
raw = excalidraw_path.read_text(encoding="utf-8")
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
print(f"ERROR: Invalid JSON in {excalidraw_path}: {e}", file=sys.stderr)
sys.exit(1)
errors = validate_excalidraw(data)
if errors:
print(f"ERROR: Invalid Excalidraw file:", file=sys.stderr)
for err in errors:
print(f" - {err}", file=sys.stderr)
sys.exit(1)
# Compute viewport size from element bounding box
elements = [e for e in data["elements"] if not e.get("isDeleted")]
min_x, min_y, max_x, max_y = compute_bounding_box(elements)
padding = 80
diagram_w = max_x - min_x + padding * 2
diagram_h = max_y - min_y + padding * 2
# Cap viewport width, let height be natural
vp_width = min(int(diagram_w), max_width)
vp_height = max(int(diagram_h), 600)
# Output path
if output_path is None:
output_path = excalidraw_path.with_suffix(".png")
# Template path (same directory as this script)
template_path = Path(__file__).parent / "render_template.html"
if not template_path.exists():
print(f"ERROR: Template not found at {template_path}", file=sys.stderr)
sys.exit(1)
template_url = template_path.as_uri()
with sync_playwright() as p:
try:
browser = p.chromium.launch(headless=True)
except Exception as e:
if "Executable doesn't exist" in str(e) or "browserType.launch" in str(e):
print("ERROR: Chromium not installed for Playwright.", file=sys.stderr)
print("Run: cd ~/.openclaw/skills/excalidraw-diagram && uv run playwright install chromium", file=sys.stderr)
sys.exit(1)
raise
page = browser.new_page(
viewport={"width": vp_width, "height": vp_height},
device_scale_factor=scale,
)
# Load the template
page.goto(template_url)
# Wait for the ES module to load (imports from esm.sh)
page.wait_for_function("window.__moduleReady === true", timeout=30000)
# Inject the diagram data and render
json_str = json.dumps(data)
result = page.evaluate(f"window.renderDiagram({json_str})")
if not result or not result.get("success"):
error_msg = result.get("error", "Unknown render error") if result else "renderDiagram returned null"
print(f"ERROR: Render failed: {error_msg}", file=sys.stderr)
browser.close()
sys.exit(1)
# Wait for render completion signal
page.wait_for_function("window.__renderComplete === true", timeout=15000)
# Screenshot the SVG element
svg_el = page.query_selector("#root svg")
if svg_el is None:
print("ERROR: No SVG element found after render.", file=sys.stderr)
browser.close()
sys.exit(1)
svg_el.screenshot(path=str(output_path))
browser.close()
return output_path
def main() -> None:
parser = argparse.ArgumentParser(description="Render Excalidraw JSON to PNG")
parser.add_argument("input", type=Path, help="Path to .excalidraw JSON file")
parser.add_argument("--output", "-o", type=Path, default=None, help="Output PNG path (default: same name with .png)")
parser.add_argument("--scale", "-s", type=int, default=2, help="Device scale factor (default: 2)")
parser.add_argument("--width", "-w", type=int, default=1920, help="Max viewport width (default: 1920)")
args = parser.parse_args()
if not args.input.exists():
print(f"ERROR: File not found: {args.input}", file=sys.stderr)
sys.exit(1)
png_path = render(args.input, args.output, args.scale, args.width)
print(str(png_path))
if __name__ == "__main__":
main()
FILE:references/render_template.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #ffffff; overflow: hidden; }
#root { display: inline-block; }
#root svg { display: block; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module">
import { exportToSvg } from "https://esm.sh/@excalidraw/excalidraw?bundle";
window.renderDiagram = async function(jsonData) {
try {
const data = typeof jsonData === "string" ? JSON.parse(jsonData) : jsonData;
const elements = data.elements || [];
const appState = data.appState || {};
const files = data.files || {};
// Force white background in appState
appState.viewBackgroundColor = appState.viewBackgroundColor || "#ffffff";
appState.exportWithDarkMode = false;
const svg = await exportToSvg({
elements: elements,
appState: {
...appState,
exportBackground: true,
},
files: files,
});
// Clear any previous render
const root = document.getElementById("root");
root.innerHTML = "";
root.appendChild(svg);
window.__renderComplete = true;
window.__renderError = null;
return { success: true, width: svg.getAttribute("width"), height: svg.getAttribute("height") };
} catch (err) {
window.__renderComplete = true;
window.__renderError = err.message;
return { success: false, error: err.message };
}
};
// Signal that the module is loaded and ready
window.__moduleReady = true;
</script>
</body>
</html>
FILE:render_excalidraw.py
"""Render Excalidraw JSON to PNG using Playwright + headless Chromium.
Usage:
cd ~/.openclaw/skills/excalidraw-diagram
uv run python render_excalidraw.py <path-to-file.excalidraw> [--output path.png] [--scale 2] [--width 1920]
First-time setup:
cd ~/.openclaw/skills/excalidraw-diagram
uv sync
uv run playwright install chromium
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
def validate_excalidraw(data: dict) -> list[str]:
"""Validate Excalidraw JSON structure. Returns list of errors (empty = valid)."""
errors: list[str] = []
if data.get("type") != "excalidraw":
errors.append(f"Expected type 'excalidraw', got '{data.get('type')}'")
if "elements" not in data:
errors.append("Missing 'elements' array")
elif not isinstance(data["elements"], list):
errors.append("'elements' must be an array")
elif len(data["elements"]) == 0:
errors.append("'elements' array is empty — nothing to render")
return errors
def compute_bounding_box(elements: list[dict]) -> tuple[float, float, float, float]:
"""Compute bounding box (min_x, min_y, max_x, max_y) across all elements."""
min_x = float("inf")
min_y = float("inf")
max_x = float("-inf")
max_y = float("-inf")
for el in elements:
if el.get("isDeleted"):
continue
x = el.get("x", 0)
y = el.get("y", 0)
w = el.get("width", 0)
h = el.get("height", 0)
# For arrows/lines, points array defines the shape relative to x,y
if el.get("type") in ("arrow", "line") and "points" in el:
for px, py in el["points"]:
min_x = min(min_x, x + px)
min_y = min(min_y, y + py)
max_x = max(max_x, x + px)
max_y = max(max_y, y + py)
else:
min_x = min(min_x, x)
min_y = min(min_y, y)
max_x = max(max_x, x + abs(w))
max_y = max(max_y, y + abs(h))
if min_x == float("inf"):
return (0, 0, 800, 600)
return (min_x, min_y, max_x, max_y)
def render(
excalidraw_path: Path,
output_path: Path | None = None,
scale: int = 2,
max_width: int = 1920,
) -> Path:
"""Render an .excalidraw file to PNG. Returns the output PNG path."""
# Import playwright here so validation errors show before import errors
try:
from playwright.sync_api import sync_playwright
except ImportError:
print("ERROR: playwright not installed.", file=sys.stderr)
print("Run: cd ~/.openclaw/skills/excalidraw-diagram && uv sync && uv run playwright install chromium", file=sys.stderr)
sys.exit(1)
# Read and validate
raw = excalidraw_path.read_text(encoding="utf-8")
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
print(f"ERROR: Invalid JSON in {excalidraw_path}: {e}", file=sys.stderr)
sys.exit(1)
errors = validate_excalidraw(data)
if errors:
print(f"ERROR: Invalid Excalidraw file:", file=sys.stderr)
for err in errors:
print(f" - {err}", file=sys.stderr)
sys.exit(1)
# Compute viewport size from element bounding box
elements = [e for e in data["elements"] if not e.get("isDeleted")]
min_x, min_y, max_x, max_y = compute_bounding_box(elements)
padding = 80
diagram_w = max_x - min_x + padding * 2
diagram_h = max_y - min_y + padding * 2
# Cap viewport width, let height be natural
vp_width = min(int(diagram_w), max_width)
vp_height = max(int(diagram_h), 600)
# Output path
if output_path is None:
output_path = excalidraw_path.with_suffix(".png")
# Template path (same directory as this script)
template_path = Path(__file__).parent / "render_template.html"
bundle_path = Path(__file__).parent / "excalidraw.iife.js"
if not template_path.exists():
print(f"ERROR: Template not found at {template_path}", file=sys.stderr)
sys.exit(1)
if not bundle_path.exists():
print("ERROR: Local Excalidraw bundle not found.", file=sys.stderr)
print("Run: bash setup.sh (from the skill directory)", file=sys.stderr)
sys.exit(1)
# Inject the local bundle path into the template (avoids CDN fetch at render time)
template_html = template_path.read_text(encoding="utf-8")
template_html = template_html.replace("__EXCALIDRAW_BUNDLE_PATH__", bundle_path.as_uri())
import tempfile
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as tmp:
tmp.write(template_html)
tmp_template_path = Path(tmp.name)
template_url = tmp_template_path.as_uri()
with sync_playwright() as p:
try:
browser = p.chromium.launch(headless=True)
except Exception as e:
if "Executable doesn't exist" in str(e) or "browserType.launch" in str(e):
print("ERROR: Chromium not installed for Playwright.", file=sys.stderr)
print("Run: cd ~/.openclaw/skills/excalidraw-diagram && uv run playwright install chromium", file=sys.stderr)
sys.exit(1)
raise
page = browser.new_page(
viewport={"width": vp_width, "height": vp_height},
device_scale_factor=scale,
)
# Load the template
page.goto(template_url)
# Wait for the local bundle to load
page.wait_for_function("window.__moduleReady === true", timeout=30000)
# Inject the diagram data and render
json_str = json.dumps(data)
result = page.evaluate(f"window.renderDiagram({json_str})")
if not result or not result.get("success"):
error_msg = result.get("error", "Unknown render error") if result else "renderDiagram returned null"
print(f"ERROR: Render failed: {error_msg}", file=sys.stderr)
browser.close()
sys.exit(1)
# Wait for render completion signal
page.wait_for_function("window.__renderComplete === true", timeout=15000)
# Screenshot the SVG element
svg_el = page.query_selector("#root svg")
if svg_el is None:
print("ERROR: No SVG element found after render.", file=sys.stderr)
browser.close()
sys.exit(1)
svg_el.screenshot(path=str(output_path))
browser.close()
tmp_template_path.unlink(missing_ok=True)
return output_path
def main() -> None:
parser = argparse.ArgumentParser(description="Render Excalidraw JSON to PNG")
parser.add_argument("input", type=Path, help="Path to .excalidraw JSON file")
parser.add_argument("--output", "-o", type=Path, default=None, help="Output PNG path (default: same name with .png)")
parser.add_argument("--scale", "-s", type=int, default=2, help="Device scale factor (default: 2)")
parser.add_argument("--width", "-w", type=int, default=1920, help="Max viewport width (default: 1920)")
args = parser.parse_args()
if not args.input.exists():
print(f"ERROR: File not found: {args.input}", file=sys.stderr)
sys.exit(1)
png_path = render(args.input, args.output, args.scale, args.width)
print(str(png_path))
if __name__ == "__main__":
main()
FILE:render_template.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #ffffff; overflow: hidden; }
#root { display: inline-block; }
#root svg { display: block; }
</style>
</head>
<body>
<div id="root"></div>
<!-- Local bundle — built by setup.sh (no CDN/network calls at render time) -->
<script src="__EXCALIDRAW_BUNDLE_PATH__"></script>
<script>
window.renderDiagram = async function(jsonData) {
try {
const data = typeof jsonData === "string" ? JSON.parse(jsonData) : jsonData;
const elements = data.elements || [];
const appState = data.appState || {};
const files = data.files || {};
appState.viewBackgroundColor = appState.viewBackgroundColor || "#ffffff";
appState.exportBackground = true;
appState.exportWithDarkMode = false;
const { exportToSvg } = ExcalidrawLib;
const svg = await exportToSvg({
elements,
appState,
files,
});
const root = document.getElementById("root");
root.innerHTML = "";
root.appendChild(svg);
window.__renderComplete = true;
window.__renderError = null;
return { success: true, width: svg.getAttribute("width"), height: svg.getAttribute("height") };
} catch (err) {
window.__renderComplete = true;
window.__renderError = err.message;
return { success: false, error: err.message };
}
};
window.__moduleReady = true;
</script>
</body>
</html>
FILE:setup.sh
#!/usr/bin/env bash
# Setup script for excalidraw-render skill.
# Run once before first use to build the local Excalidraw bundle.
# Usage: bash setup.sh
set -e
SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "Building local Excalidraw bundle..."
# Install esbuild and excalidraw in a temp dir, then bundle
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
cd "$TMP"
npm init -y > /dev/null
npm install @excalidraw/excalidraw esbuild --save-dev > /dev/null 2>&1
npx esbuild node_modules/@excalidraw/excalidraw/dist/prod/index.js \
--bundle \
--format=iife \
--global-name=ExcalidrawLib \
--minify \
--outfile="$SKILL_DIR/excalidraw.iife.js" 2>&1
echo "Done. Bundle written to: $SKILL_DIR/excalidraw.iife.js"
# Install excalidraw-cli locally so it doesn't need npx at runtime
echo "Installing @swiftlysingh/excalidraw-cli..."
npm install -g @swiftlysingh/excalidraw-cli > /dev/null 2>&1 || \
npm install --prefix "$SKILL_DIR/.npm" @swiftlysingh/excalidraw-cli > /dev/null 2>&1
echo "Done."
echo "Now run: cd $SKILL_DIR && uv sync && uv run playwright install chromium"
Provides tailored shot composition advice for vertical short videos, covering framing, angles, depth, camera movement, and smartphone setup tips.
# Short Video Shot Composition Guide
Teaches and applies shot composition principles — framing, angles, depth, and movement — specifically optimized for vertical short video formats.
## Target Users
- Beginner to intermediate videographers
- Content creators
- Smartphone shooters
- Solo creators
## When to Use
- Planning camera setups for a shoot
- Improving visual quality of existing content
- Learning composition fundamentals for vertical video
- Preparing multi-camera or multi-angle shoots
## Core Workflow
1. Aspect ratio & safe zone awareness (9:16, 1:1, 16:9)
2. Composition rule selection (rule of thirds, center framing, headroom, leading lines, symmetry, negative space)
3. Shot size & angle guide
4. Depth and layering (foreground, midground, background)
5. Camera movement dos and don'ts
6. Smartphone-specific tips
## Inputs
- Shooting environment description
- Available equipment
- Content type
- Desired visual style
## Expected Outputs
- Per-shot composition recommendations
- Framing diagrams (described textually)
- Equipment setup notes
- Common mistake avoidance checklist
## Example Prompts
- "I shoot cooking videos with my phone in a small kitchen — how should I compose my shots?"
- "Guide me through shot composition for a talking-head educational video in vertical format."
- "What composition rules should I follow for a fashion lookbook short video?"
## Trigger Keywords
shot composition, video framing, camera angles, vertical video, filming guide, composition rules
## Safety & Limitations
Composition guidance is educational. Does not control or operate camera equipment. Users should follow safety precautions when setting up equipment.
---
*Generated for project short-video-skills-2026-04-27*
FILE:skill.json
{
"slug": "sv-shot-composition",
"name": "Short Video Shot Composition Guide",
"description": "Teaches and applies shot composition principles — framing, angles, depth, and movement — specifically optimized for vertical short video formats.",
"type": "descriptive",
"requires_api": false,
"readiness": "stable",
"tags": [
"video",
"composition",
"framing",
"camera",
"shooting",
"descriptive"
],
"trigger_keywords": [
"shot composition",
"video framing",
"camera angles",
"vertical video",
"filming guide",
"composition rules"
],
"max_files": 4,
"language": "en",
"safety": "document-only informational guidance"
}
FILE:README.md
# Short Video Shot Composition Guide
Teaches and applies shot composition principles — framing, angles, depth, and movement — specifically optimized for vertical short video formats.
## Target Users
- Beginner to intermediate videographers
- Content creators
- Smartphone shooters
- Solo creators
## When to Use
- Planning camera setups for a shoot
- Improving visual quality of existing content
- Learning composition fundamentals for vertical video
- Preparing multi-camera or multi-angle shoots
## Trigger Keywords
shot composition, video framing, camera angles, vertical video, filming guide, composition rules
## Full Documentation
See [SKILL.md](./SKILL.md) for complete workflow, inputs, outputs, and examples.
---
*Generated for project short-video-skills-2026-04-27*
FILE:ACCEPTANCE.md
# Acceptance Checklist — Short Video Shot Composition Guide
## Criteria
- [x] Document-only: no handler.py, scripts, APIs, or executable code
- [x] No network calls or credential handling
- [x] English-first documentation
- [x] File count ≤ 10 (target: exactly 4)
- [x] Includes safety disclaimer
- [x] skill.json is valid with `requires_api: false`
- [x] No drift from design-spec.md
## Files in This Skill
1. `SKILL.md` — Full workflow, inputs, outputs, examples, safety
2. `README.md` — Quick-start reference
3. `skill.json` — Machine-readable metadata
4. `ACCEPTANCE.md` — This checklist
## Verification Commands
```bash
# Count files in this directory
find /Users/jianghaidong/.openclaw/skills/sv-shot-composition -type f | wc -l
# Expected: 4
# Verify skill.json
cat /Users/jianghaidong/.openclaw/skills/sv-shot-composition/skill.json | grep requires_api
# Expected: "requires_api": false
# Verify no code files
find /Users/jianghaidong/.openclaw/skills/sv-shot-composition -name "*.py" -o -name "*.sh" | wc -l
# Expected: 0
```
---
*Generated for project short-video-skills-2026-04-27*
Guides video creators to plan shot-by-shot storyboards with framing, composition, overlays, timing, and camera movements for short videos.
# Short Video Storyboard Planner
Guides creators through visual storyboarding — shot-by-shot planning with framing, composition, text overlays, and timing notes.
## Target Users
- Video creators
- Directors
- Content producers
- Marketing video teams
- Beginner filmmakers
## When to Use
- Pre-production planning for complex shots
- Ensuring visual variety across cuts
- Communicating vision to collaborators or clients
- Planning transitions between scenes
## Core Workflow
1. Script-to-shot breakdown
2. Shot type selection (wide, medium, close-up, extreme close-up, POV, over-shoulder)
3. Camera movement planning (static, pan, tilt, dolly, handheld, drone)
4. Composition notes (rule of thirds, leading lines, headroom)
5. Text/graphic overlay planning
6. Timing and transition notes
## Inputs
- Complete script or scene outline
- Total target duration
- Platform aspect ratio requirements
- Visual style references
## Expected Outputs
- Shot-by-shot storyboard table
- Visual style guidance summary
## Example Prompts
- "Storyboard my 60-second product unboxing script for Douyin vertical format."
- "Break down this cooking tutorial script into a shot-by-shot storyboard."
- "Create a storyboard for a brand storytelling video with 12 shots, 45 seconds."
## Trigger Keywords
storyboard, shot list, shot planning, visual plan, pre-production, shot breakdown
## Safety & Limitations
Storyboard guidance is creative support. Does not generate actual visual assets, drawings, or animations. Shot descriptions are textual planning aids.
---
*Generated for project short-video-skills-2026-04-27*
FILE:skill.json
{
"slug": "sv-storyboard-planner",
"name": "Short Video Storyboard Planner",
"description": "Guides creators through visual storyboarding — shot-by-shot planning with framing, composition, text overlays, and timing notes.",
"type": "descriptive",
"requires_api": false,
"readiness": "stable",
"tags": [
"video",
"storyboard",
"pre-production",
"planning",
"visual",
"descriptive"
],
"trigger_keywords": [
"storyboard",
"shot list",
"shot planning",
"visual plan",
"pre-production",
"shot breakdown"
],
"max_files": 4,
"language": "en",
"safety": "document-only informational guidance"
}
FILE:README.md
# Short Video Storyboard Planner
Guides creators through visual storyboarding — shot-by-shot planning with framing, composition, text overlays, and timing notes.
## Target Users
- Video creators
- Directors
- Content producers
- Marketing video teams
- Beginner filmmakers
## When to Use
- Pre-production planning for complex shots
- Ensuring visual variety across cuts
- Communicating vision to collaborators or clients
- Planning transitions between scenes
## Trigger Keywords
storyboard, shot list, shot planning, visual plan, pre-production, shot breakdown
## Full Documentation
See [SKILL.md](./SKILL.md) for complete workflow, inputs, outputs, and examples.
---
*Generated for project short-video-skills-2026-04-27*
FILE:ACCEPTANCE.md
# Acceptance Checklist — Short Video Storyboard Planner
## Criteria
- [x] Document-only: no handler.py, scripts, APIs, or executable code
- [x] No network calls or credential handling
- [x] English-first documentation
- [x] File count ≤ 10 (target: exactly 4)
- [x] Includes safety disclaimer
- [x] skill.json is valid with `requires_api: false`
- [x] No drift from design-spec.md
## Files in This Skill
1. `SKILL.md` — Full workflow, inputs, outputs, examples, safety
2. `README.md` — Quick-start reference
3. `skill.json` — Machine-readable metadata
4. `ACCEPTANCE.md` — This checklist
## Verification Commands
```bash
# Count files in this directory
find /Users/jianghaidong/.openclaw/skills/sv-storyboard-planner -type f | wc -l
# Expected: 4
# Verify skill.json
cat /Users/jianghaidong/.openclaw/skills/sv-storyboard-planner/skill.json | grep requires_api
# Expected: "requires_api": false
# Verify no code files
find /Users/jianghaidong/.openclaw/skills/sv-storyboard-planner -name "*.py" -o -name "*.sh" | wc -l
# Expected: 0
```
---
*Generated for project short-video-skills-2026-04-27*
Creates 3–5 attention-capturing first-3-second hooks for short videos with visual and verbal suggestions to boost retention and engagement.
# Short Video Hook Crafter
Specializes in crafting attention-grabbing first-3-second hooks — visual, verbal, and pattern-interrupt — for short videos.
## Target Users
- Content creators
- Video editors
- Scriptwriters
- Social media marketers
## When to Use
- Low retention in the first 3 seconds
- Brainstorming multiple hook options for a single video
- Designing pattern-interrupt openings
- A/B testing hook variations
## Core Workflow
1. Content goal & emotion mapping
2. Hook type selection (curiosity gap, bold claim, question, pattern interrupt, visual surprise, relatable moment)
3. Hook drafting (3–5 variations)
4. Visual/text overlay coordination notes
5. Hook scoring rubric
6. First-frame / thumbnail alignment check
## Inputs
- Video topic
- Target audience
- Content format
- Platform norms
## Expected Outputs
- 3–5 hook variations per topic
- Visual/text overlay suggestions
- Scorecard with rationale
- Frame-1 alignment notes
## Example Prompts
- "Give me 5 hooks for a cooking video about making the perfect scrambled eggs."
- "My retention drops at 2 seconds — help me fix my hook for a tech review video."
- "Craft pattern-interrupt hooks for a finance education short targeting Gen Z."
## Trigger Keywords
video hook, attention hook, first 3 seconds, hook ideas, retention hook, pattern interrupt
## Safety & Limitations
Hooks are creative suggestions. Clickbait or misleading hooks that violate platform policies are not endorsed. Creator bears responsibility for honest representation.
---
*Generated for project short-video-skills-2026-04-27*
FILE:skill.json
{
"slug": "sv-hook-crafter",
"name": "Short Video Hook Crafter",
"description": "Specializes in crafting attention-grabbing first-3-second hooks — visual, verbal, and pattern-interrupt — for short videos.",
"type": "descriptive",
"requires_api": false,
"readiness": "stable",
"tags": [
"video",
"hook",
"engagement",
"attention",
"script",
"descriptive"
],
"trigger_keywords": [
"video hook",
"attention hook",
"first 3 seconds",
"hook ideas",
"retention hook",
"pattern interrupt"
],
"max_files": 4,
"language": "en",
"safety": "document-only informational guidance"
}
FILE:README.md
# Short Video Hook Crafter
Specializes in crafting attention-grabbing first-3-second hooks — visual, verbal, and pattern-interrupt — for short videos.
## Target Users
- Content creators
- Video editors
- Scriptwriters
- Social media marketers
## When to Use
- Low retention in the first 3 seconds
- Brainstorming multiple hook options for a single video
- Designing pattern-interrupt openings
- A/B testing hook variations
## Trigger Keywords
video hook, attention hook, first 3 seconds, hook ideas, retention hook, pattern interrupt
## Full Documentation
See [SKILL.md](./SKILL.md) for complete workflow, inputs, outputs, and examples.
---
*Generated for project short-video-skills-2026-04-27*
FILE:ACCEPTANCE.md
# Acceptance Checklist — Short Video Hook Crafter
## Criteria
- [x] Document-only: no handler.py, scripts, APIs, or executable code
- [x] No network calls or credential handling
- [x] English-first documentation
- [x] File count ≤ 10 (target: exactly 4)
- [x] Includes safety disclaimer
- [x] skill.json is valid with `requires_api: false`
- [x] No drift from design-spec.md
## Files in This Skill
1. `SKILL.md` — Full workflow, inputs, outputs, examples, safety
2. `README.md` — Quick-start reference
3. `skill.json` — Machine-readable metadata
4. `ACCEPTANCE.md` — This checklist
## Verification Commands
```bash
# Count files in this directory
find /Users/jianghaidong/.openclaw/skills/sv-hook-crafter -type f | wc -l
# Expected: 4
# Verify skill.json
cat /Users/jianghaidong/.openclaw/skills/sv-hook-crafter/skill.json | grep requires_api
# Expected: "requires_api": false
# Verify no code files
find /Users/jianghaidong/.openclaw/skills/sv-hook-crafter -name "*.py" -o -name "*.sh" | wc -l
# Expected: 0
```
---
*Generated for project short-video-skills-2026-04-27*
Creates timed, structured short-video scripts using proven frameworks with hooks, visual notes, and CTA for content creators and marketers.
# Short Video Script Builder
Builds structured short-video scripts using proven frameworks (hook-body-CTA, AIDA, PAS, story arc) with timing and visual notes.
## Target Users
- Content creators
- Video marketers
- Educators
- Storytellers
- Scriptwriters
## When to Use
- Writing a script from scratch for a new video
- Structuring raw ideas into a coherent narrative
- Adapting a long-form piece into a short video
## Core Workflow
1. Video objective and target emotion selection
2. Structure framework selection (AIDA, PAS, Hook-Story-CTA, Problem-Solution, Listicle)
3. Hook development (first 3 seconds)
4. Body / story / value delivery
5. CTA (call to action) design
6. Timing allocation per segment
## Inputs
- Topic
- Target length (15s/30s/60s/90s+)
- Platform
- Target audience
- Video objective
## Expected Outputs
- Structured script with timestamps
- Hook drafts (multiple options)
- B-roll and visual cue notes
- CTA variations
## Example Prompts
- "Write a 60-second script for a Douyin product demo using the Problem-Solution framework."
- "Build a 30-second storytelling script about my morning routine for a lifestyle channel."
- "Adapt this blog post into 3 short video scripts for different platforms."
## Trigger Keywords
video script, script template, script structure, write video, short video script, AIDA script, hook script
## Safety & Limitations
Script structures are templates and guidance. All final content is the creator's responsibility. No automated video generation or text-to-speech.
---
*Generated for project short-video-skills-2026-04-27*
FILE:skill.json
{
"slug": "sv-script-builder",
"name": "Short Video Script Builder",
"description": "Builds structured short-video scripts using proven frameworks (hook-body-CTA, AIDA, PAS, story arc) with timing and visual notes.",
"type": "descriptive",
"requires_api": false,
"readiness": "stable",
"tags": [
"video",
"script",
"writing",
"storytelling",
"structure",
"descriptive"
],
"trigger_keywords": [
"video script",
"script template",
"script structure",
"write video",
"short video script",
"AIDA script",
"hook script"
],
"max_files": 4,
"language": "en",
"safety": "document-only informational guidance"
}
FILE:README.md
# Short Video Script Builder
Builds structured short-video scripts using proven frameworks (hook-body-CTA, AIDA, PAS, story arc) with timing and visual notes.
## Target Users
- Content creators
- Video marketers
- Educators
- Storytellers
- Scriptwriters
## When to Use
- Writing a script from scratch for a new video
- Structuring raw ideas into a coherent narrative
- Adapting a long-form piece into a short video
## Trigger Keywords
video script, script template, script structure, write video, short video script, AIDA script, hook script
## Full Documentation
See [SKILL.md](./SKILL.md) for complete workflow, inputs, outputs, and examples.
---
*Generated for project short-video-skills-2026-04-27*
FILE:ACCEPTANCE.md
# Acceptance Checklist — Short Video Script Builder
## Criteria
- [x] Document-only: no handler.py, scripts, APIs, or executable code
- [x] No network calls or credential handling
- [x] English-first documentation
- [x] File count ≤ 10 (target: exactly 4)
- [x] Includes safety disclaimer
- [x] skill.json is valid with `requires_api: false`
- [x] No drift from design-spec.md
## Files in This Skill
1. `SKILL.md` — Full workflow, inputs, outputs, examples, safety
2. `README.md` — Quick-start reference
3. `skill.json` — Machine-readable metadata
4. `ACCEPTANCE.md` — This checklist
## Verification Commands
```bash
# Count files in this directory
find /Users/jianghaidong/.openclaw/skills/sv-script-builder -type f | wc -l
# Expected: 4
# Verify skill.json
cat /Users/jianghaidong/.openclaw/skills/sv-script-builder/skill.json | grep requires_api
# Expected: "requires_api": false
# Verify no code files
find /Users/jianghaidong/.openclaw/skills/sv-script-builder -name "*.py" -o -name "*.sh" | wc -l
# Expected: 0
```
---
*Generated for project short-video-skills-2026-04-27*
Generate professional Markdown invoices with multi-currency support, tax, discounts, payment details, and clear formatting for freelancers and small businesses.
# Invoice Generator Create professional invoices in Markdown format. Supports multiple currencies, tax calculations, and standard invoice fields. Perfect for freelancers, consultants, and small businesses. ## When to use Use this skill when the user needs to: - Create a professional invoice for a client - Generate recurring invoices - Calculate totals with tax, discounts, and multiple line items - Format an invoice in a printable Markdown layout - Convert invoice details into a structured document ## How it works 1. Ask for sender details (business name, address, email, payment info) 2. Ask for recipient details (client name, company, address) 3. Ask for line items (description, quantity, unit price) 4. Ask for currency, tax rate, discount, and payment terms 5. Generate a complete professional invoice in Markdown ## Invoice Template ```markdown # INVOICE **Invoice #:** [AUTO-INCREMENT or USER-PROVIDED] **Date:** [CURRENT DATE] **Due Date:** [DATE + PAYMENT TERMS] --- **From:** [Business Name] [Address Line 1] [Address Line 2] [Email] | [Phone] **Bill To:** [Client Name] [Client Company] [Client Address] [Client Email] --- ## Items | # | Description | Qty | Unit Price | Amount | |---|-------------|-----|-----------|--------| | 1 | [Service/Product] | [Qty] | [Price] | [Total] | | 2 | [Service/Product] | [Qty] | [Price] | [Total] | --- | | | |---|---| | **Subtotal** | [CURRENCY] [AMOUNT] | | **Tax ([RATE]%)** | [CURRENCY] [AMOUNT] | | **Discount** | -[CURRENCY] [AMOUNT] | | **TOTAL DUE** | **[CURRENCY] [AMOUNT]** | --- ## Payment Details **Payment Method:** [Bank Transfer / PayPal / Stripe / etc.] **Bank:** [Bank Name] **Account:** [Account Number] **Routing:** [Routing Number] **Payment Terms:** [Net 30 / Due on Receipt / etc.] --- *Thank you for your business!* ``` ## Supported Currencies USD ($), EUR (€), GBP (£), JPY (¥), CAD (C$), AUD (A$), CHF, INR (₹), BRL (R$), KRW (₩), and more. Format amounts according to locale conventions. ## Calculation Rules - Subtotal = Sum of (quantity × unit price) for all line items - Tax = Subtotal × tax rate - Discount can be percentage or fixed amount - Total = Subtotal + Tax - Discount - Round to 2 decimal places (0 for JPY/KRW) ## Output Generate a clean Markdown invoice that: - Is print-ready when rendered - Uses proper currency formatting - Includes all required fields - Has clear visual hierarchy with horizontal rules - Can be easily copied and converted to PDF