Initial commit: document_analyzer with CI/CD pipeline
CI / test (push) Successful in 30s

- 4 skill pipeline (doc_parser, conflict_detection, ir_generation, resolution_application)
- CI workflow on push/PR (.gitea/workflows/ci.yml)
- Auto-issue on CI failure (.gitea/workflows/auto-issue.yml)
- Pytest smoke tests (tests/test_sample.py)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 20:00:26 +08:00
commit 40567a4fb6
22 changed files with 2898 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
---
name: IR生成技能
description: 从处理后的文档生成结构化的JSON中间表示,合并冲突解决方案并维护源可追溯性。
---
# IR生成技能
## 概述
此技能从处理后的文档创建结构化的JSON中间表示(IR)。它在章节级别工作以提供全局上下文,合并冲突解决方案,并维护所有生成内容的源可追溯性。
## 功能
该技能:
- 在章节级别而非小块级别处理文档
- 在生成过程中合并冲突解决方案
- 生成文档内容的结构化JSON表示
- 维护包含章节和位置信息的源可追溯性
- 通过智能拆分大型章节同时保留冲突上下文来处理大型章节
- 确保处理过程中保持全局上下文
## 输入要求
- 更新文档JSON文件的路径(包含应用的解决方案)
- 可选输出目录规范
- 可选试运行标志,在不调用API的情况下预览大语言模型提示
## 输出
该技能生成一个结构化JSON文件,文件名为输入文档的基本名称后跟'_ir.json',包含:
- 文档内容的结构化表示
- 源跟踪信息(章节和位置)
- 转换字段中的清理和解析的JSON数据
- 适合下游处理的格式正确的中间表示
## 处理详情
- 每个完整章节(文本+图像)一起发送给大语言模型以获得全局上下文
- 解决的冲突更正注入到提示中,因此大语言模型使用校正后的值
- 超过约3000个标记的章节会智能拆分,同时在每个块中保留冲突上下文
- 每个IR条目包括source.section + source.location以实现可追溯性
- 处理后的内容以JSON格式结构化,便于机器读取
## 集成点
此技能消耗解决方案应用技能的输出并为文档分析管道生成最终结构化输出。
+105
View File
@@ -0,0 +1,105 @@
import logging
import os
import time
from typing import Optional
from openai import OpenAI
logger = logging.getLogger(__name__)
class LLMClient:
"""Low-level OpenAI-compatible LLM client with retry and token tracking.
Usage::
llm = LLMClient()
content = llm.chat("qwen3.5-flash", [{"role": "user", "content": "Hello"}])
print(llm.usage)
"""
IMAGE_MODEL = "qwen3-vl-plus"
TEXT_MODEL = "qwen3.5-flash-2026-02-23"
TIMEOUT = 120
MAX_RETRIES = 3
def __init__(
self,
*,
base_url: str = "https://dashscope.aliyuncs.com/compatible-mode/v1",
timeout: int | None = None,
):
key = os.environ.get("DASHSCOPE_API_KEY", "")
if not key:
raise ValueError("DASHSCOPE_API_KEY environment variable is not set.")
self._client = OpenAI(api_key=key, base_url=base_url)
self._timeout = timeout or self.TIMEOUT
self._prompt_tokens = 0
self._completion_tokens = 0
@property
def usage(self) -> dict:
"""Return accumulated token counts as ``{prompt, completion, total}``."""
return {
"prompt_tokens": self._prompt_tokens,
"completion_tokens": self._completion_tokens,
"total_tokens": self._prompt_tokens + self._completion_tokens,
}
@staticmethod
def estimate_tokens(text: str) -> int:
"""Quick token estimate. CJK ≈1.7/token, others ≈3.0/token."""
cjk = sum(1 for c in text if '' <= c <= '鿿' or ' ' <= c <= '')
other = len(text) - cjk
return max(1, int(cjk / 1.7 + other / 3.0))
@staticmethod
def estimate_image_tokens() -> int:
"""Fixed estimate for one vision-model image (~500 tokens)."""
return 500
def chat(
self, model: str, messages: list[dict], *, timeout: int | None = None,
response_format: dict | None = None,
) -> str:
"""Send a chat completion request and return the response content.
Automatically retries on failure and accumulates token usage.
"""
label = f"chat({model})"
def _call():
t0 = time.time()
kwargs = dict(model=model, messages=messages, timeout=timeout or self._timeout)
if response_format is not None:
kwargs["response_format"] = response_format
kwargs["temperature"] = 0
resp = self._client.chat.completions.create(**kwargs)
content = resp.choices[0].message.content
usg = resp.usage
if usg:
self._prompt_tokens += usg.prompt_tokens
self._completion_tokens += usg.completion_tokens
elapsed = time.time() - t0
logger.info("%s: %d chars in %.1fs", label, len(content) if content else 0, elapsed)
if not content:
raise RuntimeError("Empty response from LLM")
return content
return self._retry(_call, label)
def _retry(self, fn, label: str) -> str:
"""Call *fn()* with exponential-backoff retry."""
last_error: Optional[Exception] = None
for attempt in range(self.MAX_RETRIES):
try:
return fn()
except Exception as e:
last_error = e
logger.warning(
"%s error (attempt %d/%d): %s",
label, attempt + 1, self.MAX_RETRIES, e,
)
if attempt < self.MAX_RETRIES - 1:
time.sleep(2 ** attempt)
raise RuntimeError(f"{label}: all retries exhausted") from last_error
@@ -0,0 +1,359 @@
#!/usr/bin/env python3
"""Generate JSON intermediate representation from ``_parsed.json`` or ``_updated.json``.
Sends the JSON document directly to the LLM for analysis. If the document exceeds
``MAX_ANALYSIS_TOKENS``, sections are batched greedily without splitting any
individual section. Conflict corrections from ``resolved_conflicts`` are included
so the output respects user arbitration decisions.
Usage::
python scripts/ir_generator.py output/<basename>_updated.json [output_dir] [--dry-run]
Output: ``<basename>_ir.json``
"""
import argparse
import json
import logging
import os
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from LLM import LLMClient
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
RATE_LIMIT_DELAY = 0.5
MAX_ANALYSIS_TOKENS = 6000 # max content size per LLM call
# ---------------------------------------------------------------------------
# Prompt
# ---------------------------------------------------------------------------
PROMPT = """你是一个需求文档分析助手。请分析以下需求文档的JSON内容,输出结构化JSON。
## 已知修正(来自冲突检测)
以下内容已确认修正,生成JSON时请**使用修正后的值**,不要同时输出两个版本。
{conflict_context}
## 待分析内容(JSON格式)
{content}
## JSON字段说明
- sections: 文档章节列表,每个章节含 source(章节标题)和 blocks(内容块数组)
- blocks: 类型含 para(段落,字段 text)和 table(表格,字段 rows,每行含 columns 数组)
- image_sources: 图片所在章节映射,key 为图片 rid
- image_analysis: 图片分析结果,每个含 rid、type(流程图/架构图/状态图等)、description
- resolved_conflicts: 已知修正列表,每个含 section、conflict_type、correction、source
## 功能点定义
只有满足以下**全部条件**的才视为功能点:
1. 描述了一个**系统或软件要实现的具体行为**(有触发条件、执行动作、状态变化或逻辑规则)
2. 该行为直接由**系统或框架**执行(不是人的操作流程、管理流程)
3. 对用户或系统有**可观察的效果**
**以下内容不是功能点,不要输出:**
- 术语/缩略词定义(
- 文档背景、范围说明(如 "本文档涵盖xxx"
- 变更日志、版本记录、编制人信息
- 文档结构描述(如 "产品简介用户场景说明"
- 纯文本的概述、没有具体行为的介绍
## 决策树/流程图分解规则(重要)
图片分析(image_analysis)中的流程图和决策树描述包含丰富的功能逻辑,**必须完全分解**:
1. **每个叶子路径 = 一个独立 function**:从根节点到每个最终结果的完整路径,都拆成一个 function
2. **每个判断分支 = 一个独立 function**:菱形判断节点的每个分支方向和对应的结果,单独作为一个 function
3. **不同约束条件 = 不同 function**:例如"通过接入SDK限制""通过系统限制"是不同约束机制,必须分别列出
4. **不要合并不同路径**:即使最终结果相同,只要到达路径不同,就是不同的 function
## 输出格式
只输出功能点,每个功能点格式如下:
{
"function": "功能名称",
"source": {
"section": "章节名",
"location": "原文位置(如:正文第1段、表格1第2行、图片rId13)"
},
"trigger": {
"type": "AND或者OR",
"conditions": [
"触发条件1",
"触发条件2"
]
},
"actions": {
"场景/角色": [
"动作1",
"动作2"
]
}
}
## 输出原则
1. **只输出功能点**,没有功能点就输出空数组 []
2. 每个功能点**必须**包含 source.section 和 source.location
3. location 必须是具体的原文位置标签(如 "正文第1段""表格1""图片rId13"
4. **一个 function 只对应一种行为逻辑(一条完整路径)**。决策树中的每个分支路径(从根到叶子)必须拆成独立 function,conditions 中明确写出该路径上的所有判断条件和分支方向。
5. **穷举所有分支**:流程图/决策树中的每一条分支路径都要输出对应的 function,不能遗漏任何子逻辑。
6. 没有 trigger 或 actions 的字段直接**省略**,不要写 null 或空列表/空对象
7. 所有功能点全部列出,**宁多勿漏**
8. **已知修正**中确认的信息,使用修正后的值
9. 输出一个JSON数组,不要用 ```json 代码块包裹,直接输出纯JSON
"""
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _parse_llm_response(raw: str) -> list | dict | str | None:
"""Parse JSON from LLM response, handling markdown code fences."""
if raw is None:
return None
stripped = raw.strip()
if stripped.startswith("```"):
nl = stripped.find("\n")
stripped = stripped[nl + 1:] if nl != -1 else stripped[3:]
if stripped.endswith("```"):
stripped = stripped[:-3]
try:
return json.loads(stripped)
except json.JSONDecodeError:
logger.warning(" Failed to parse JSON, returning raw text")
return raw
def _build_conflict_context(
section_name: str | None,
resolved_conflicts: list[dict],
) -> str:
"""Build conflict correction context for a section, or all if section_name is None."""
if section_name is None:
relevant = resolved_conflicts
else:
relevant = [c for c in resolved_conflicts if c.get("section", "") == section_name]
if not relevant:
return "没有"
lines: list[str] = []
for c in relevant:
correction = c.get("correction", "")
conflict_type = c.get("conflict_type", "")
source = c.get("source", "")
lines.append(f"- 冲突类型:{conflict_type},依据:{source}")
lines.append(f" 修正后的值:{correction}")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# LLM analysis
# ---------------------------------------------------------------------------
def _analyze_content(
content: str,
conflict_context: str,
llm: LLMClient,
*,
dry_run: bool = False,
) -> list[dict]:
"""Send content to the LLM and return IR entries."""
prompt = PROMPT.replace("{conflict_context}", conflict_context).replace("{content}", content)
if dry_run:
est = llm.estimate_tokens(prompt)
logger.info(" [DRY RUN] prompt ~%d tokens", est)
return []
try:
raw = llm.chat(
model=LLMClient.TEXT_MODEL,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
)
logger.info(" Response: %d chars", len(raw))
except RuntimeError as e:
logger.error(" Analysis failed: %s", e)
return []
parsed = _parse_llm_response(raw)
if isinstance(parsed, list):
return parsed
elif isinstance(parsed, dict):
return [parsed]
else:
logger.warning(" Unparseable response, raw length: %d", len(raw))
return []
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def generate_ir(
parsed_path: str,
output_dir: str = "output",
*,
dry_run: bool = False,
) -> dict:
"""Read parsed/updated JSON and generate JSON IR.
Produces ``<basename>_ir.json`` in *output_dir*.
"""
with open(parsed_path, "r", encoding="utf-8") as f:
data = json.load(f)
basename = os.path.splitext(os.path.basename(parsed_path))[0]
for suffix in ("_parsed", "_updated"):
if basename.endswith(suffix):
basename = basename[:-len(suffix)]
break
os.makedirs(output_dir, exist_ok=True)
llm = LLMClient()
ir_output: list[dict] = []
sections = data.get("sections", [])
image_sources = data.get("image_sources", {})
image_analysis = data.get("image_analysis", [])
resolved_conflicts = data.get("resolved_conflicts", [])
# Build full document JSON to measure size
full_doc = {
"sections": sections,
"image_sources": image_sources,
"image_analysis": image_analysis,
}
full_json = json.dumps(full_doc, ensure_ascii=False)
total_chars = len(full_json)
logger.info("Total document JSON chars: %d", total_chars)
if total_chars < MAX_ANALYSIS_TOKENS:
logger.info("Document fits in one request (< %d chars)", MAX_ANALYSIS_TOKENS)
conflict_ctx = _build_conflict_context(None, resolved_conflicts)
entries = _analyze_content(full_json, conflict_ctx, llm, dry_run=dry_run)
ir_output.extend(entries)
else:
logger.info("Document is large (>= %d chars), batching sections", MAX_ANALYSIS_TOKENS)
# Filter to non-empty sections, measure effective size per section
# (section JSON + image_sources + image_analysis for images in that section)
sec_sizes = []
for sec in sections:
if not sec.get("blocks"):
continue
sec_json = json.dumps(sec, ensure_ascii=False)
sec_chars = len(sec_json)
# Add image overhead for this section
sec_name = sec.get("source", "")
sec_rids = [rid for rid, src in image_sources.items()
if src.get("section", "") == sec_name]
if sec_rids:
overhead_doc = {
"image_sources": {rid: image_sources[rid] for rid in sec_rids},
"image_analysis": [img for img in image_analysis
if img.get("rid", "") in sec_rids],
}
sec_chars += len(json.dumps(overhead_doc, ensure_ascii=False))
sec_sizes.append((sec, sec_chars))
# Greedy batch: never split a section, keep adding until next exceeds limit
i = 0
while i < len(sec_sizes):
batch = []
batch_size = 0
while i < len(sec_sizes) and batch_size + sec_sizes[i][1] <= MAX_ANALYSIS_TOKENS:
batch.append(sec_sizes[i][0])
batch_size += sec_sizes[i][1]
i += 1
if not batch:
i += 1
continue
# Collect sections and their images for this batch
batch_names = [s.get("source", "") for s in batch]
batch_image_sources = {
rid: src for rid, src in image_sources.items()
if src.get("section", "") in batch_names
}
batch_images = [
img for img in image_analysis
if image_sources.get(img.get("rid", ""), {}).get("section", "") in batch_names
]
batch_doc = {
"sections": batch,
"image_sources": batch_image_sources,
"image_analysis": batch_images,
}
batch_json = json.dumps(batch_doc, ensure_ascii=False)
# Merge conflict contexts
ctx_parts = []
for sn in batch_names:
ctx = _build_conflict_context(sn, resolved_conflicts)
if ctx != "没有":
ctx_parts.append(ctx)
conflict_ctx = "\n".join(ctx_parts) if ctx_parts else "没有"
label = " + ".join(batch_names)
logger.info("Batch [%s]: %d sections, %d chars", label, len(batch), len(batch_json))
entries = _analyze_content(batch_json, conflict_ctx, llm, dry_run=dry_run)
ir_output.extend(entries)
time.sleep(RATE_LIMIT_DELAY)
# ---- save ----------------------------------------------------------------
ir_path = os.path.join(output_dir, f"{basename}_ir.json")
os.makedirs(os.path.dirname(ir_path) or ".", exist_ok=True)
with open(ir_path, "w", encoding="utf-8") as f:
json.dump(ir_output, f, ensure_ascii=False, indent=2)
logger.info("Saved: %s (%d entries)", ir_path, len(ir_output))
# ---- summary -------------------------------------------------------------
usg = llm.usage
logger.info("Tokens: %d prompt + %d completion = %d total",
usg["prompt_tokens"], usg["completion_tokens"], usg["total_tokens"])
logger.info("Output: %s", ir_path)
return {"ir": ir_output, "path": ir_path}
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate JSON intermediate representation from parsed/updated JSON.",
)
parser.add_argument("input", metavar="parsed.json",
help="Path to _parsed.json or _updated.json")
parser.add_argument("output_dir", nargs="?", default="output", metavar="output_dir",
help="Directory for output files (default: output/)")
parser.add_argument("--dry-run", action="store_true",
help="Print token estimates without calling the API.")
args = parser.parse_args()
generate_ir(args.input, args.output_dir, dry_run=args.dry_run)
@@ -0,0 +1,49 @@
import argparse
import sys
import yaml
def print_ir(yaml_path: str) -> None:
with open(yaml_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
if not isinstance(data, list):
print(f"Expected a YAML list, got {type(data).__name__}")
sys.exit(1)
for i, item in enumerate(data):
yc = item.get("converted")
if yc is None:
continue
# yaml_content is a raw string that happens to look like YAML —
# parse it then pretty-print it back
if isinstance(yc, str):
stripped = yc.strip()
# Strip markdown code fences: ```yaml / ``` at start, ``` at end
if stripped.startswith("```"):
first_newline = stripped.find("\n")
stripped = stripped[first_newline + 1:] if first_newline != -1 else stripped[3:]
if stripped.endswith("```"):
stripped = stripped[:-3]
try:
parsed = yaml.safe_load(stripped)
except yaml.YAMLError:
print(yc)
raise
else:
parsed = yc
if isinstance(parsed, (dict, list)):
print(yaml.dump(parsed, allow_unicode=True, default_flow_style=False, sort_keys=False).rstrip())
else:
print(str(parsed))
print("=" * 60)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Pretty-print yaml_content fields from an analysis YAML file.")
parser.add_argument("input", metavar="file.yaml", help="Path to a _文字.yaml or _图片.yaml file")
args = parser.parse_args()
print_ir(args.input)
@@ -0,0 +1,907 @@
{
"source": "D:\\projects\\jike\\车机娱乐系统禁止功能文档_精简.docx",
"sections": [
{
"source": "XX Auto行车娱乐限制功能PRD _ V1.0",
"blocks": [
{
"type": "para",
"index": 1,
"text": "功能号:SW-55-33-22"
}
],
"images": []
},
{
"source": "编制/变更日志",
"blocks": [
{
"type": "para",
"index": 1,
"text": "包含变更时间、变更内容简述、编制人"
},
{
"type": "table",
"table": 1,
"headers": [
"时间",
"版本号",
"作者",
"主要变更内容"
],
"rows": [
{
"columns": [
{
"name": "时间",
"row": 1,
"col": 1,
"text": "2025-11-11"
},
{
"name": "版本号",
"row": 1,
"col": 2,
"text": "1.0"
},
{
"name": "作者",
"row": 1,
"col": 3,
"text": "A"
},
{
"name": "主要变更内容",
"row": 1,
"col": 4,
"text": "初版撰写"
}
]
}
]
}
],
"images": []
},
{
"source": "1.1 文档背景",
"blocks": [
{
"type": "para",
"index": 1,
"text": "本文应用于XX集团的xx Auto系统行车娱乐限制功能的需求文档,详细描述系统在行车状态下对于视频、游戏等限制的场景分析,应用流程,详细策略设计等。项目实现过程中,请以详细设计为基础进行需求实现,如遇到歧义或变更,请及时沟通。"
}
],
"images": []
},
{
"source": "1.2 文档范围",
"blocks": [
{
"type": "para",
"index": 1,
"text": "当前文档包括了国内xx Auto系统行车娱乐限制功能的产品定义,以及海外车型的平台化系统行车娱乐限制功能的产品定义(仅针对视频、游戏等开车完全不能使用APP所有功能的场景)"
},
{
"type": "para",
"index": 2,
"text": "不包括部分在应用内开发的行车娱乐限制功能的定义,例如图库APP内的视频播放功能、多屏同看、DVR行车记录仪等功能(这些业务属于部分功能需要进行行车娱乐限制,相关定义在各模块的PRD内进行描述)"
}
],
"images": []
},
{
"source": "1.3 术语解释",
"blocks": [
{
"type": "table",
"table": 1,
"headers": [
"术语 / 缩略词",
"说明"
],
"rows": [
{
"columns": [
{
"name": "术语 / 缩略词",
"row": 1,
"col": 1,
"text": "CSD"
},
{
"name": "说明",
"row": 1,
"col": 2,
"text": "中控屏"
}
]
},
{
"columns": [
{
"name": "术语 / 缩略词",
"row": 2,
"col": 1,
"text": "PSD"
},
{
"name": "说明",
"row": 2,
"col": 2,
"text": "副驾屏"
}
]
},
{
"columns": [
{
"name": "术语 / 缩略词",
"row": 3,
"col": 1,
"text": "RFD(左/右)"
},
{
"name": "说明",
"row": 3,
"col": 2,
"text": "后排吸顶屏"
}
]
}
]
}
],
"images": []
},
{
"source": "1.4.1 当前功能相关文档",
"blocks": [
{
"type": "para",
"index": 1,
"text": "无"
}
],
"images": []
},
{
"source": "1.4.2 政策法规文件",
"blocks": [
{
"type": "para",
"index": 1,
"text": "方案总结:"
},
{
"type": "table",
"table": 1,
"headers": [
"列1",
"列2",
"列3"
],
"rows": [
{
"columns": [
{
"name": "列1",
"row": 1,
"col": 1,
"text": "方案总结"
},
{
"name": "列2",
"row": 1,
"col": 2,
"text": "CSD"
},
{
"name": "列3",
"row": 1,
"col": 3,
"text": "中国大陆:上“行车娱乐限制”功能开关,允许用户打开/关闭此功能\n其余国家及地区(全球范围内):上“行车娱乐限制”功能,并且强制打开"
}
]
},
{
"columns": [
{
"name": "列1",
"row": 2,
"col": 1,
"text": "方案总结"
},
{
"name": "列2",
"row": 2,
"col": 2,
"text": "PSD"
},
{
"name": "列3",
"row": 2,
"col": 3,
"text": "巴西、加纳、新加坡:上“行车娱乐限制”功能,并强制打开,无需配置“防窥膜”;\n韩国、南非、美国、新西兰:1. 若考虑到用户体验,则配置“防窥膜”,无需“行车娱乐限制”;2. 若考虑到成本(一个防窥膜400RMB),则只上“行车娱乐限制”,无需“防窥膜”\n其余国家及地区(全球范围内):可视同中国大陆方案\n香港、台湾、毛里求斯市场:不能预装任何视频软件(如Youtube)、游戏、浏览器、短信通讯软件(如Wechat、SMS/Message)、相册、相机。同时若具备RADIO、MUSIC,则不能显示歌词。CPAA映射不在考虑范围,属于客户行为,不做管控。"
}
]
}
]
},
{
"type": "para",
"index": 2,
"text": "相关法规可参考法规指导书:座舱域法规指导书"
}
],
"images": []
},
{
"source": "1.4.3 行业规范文件",
"blocks": [
{
"type": "para",
"index": 1,
"text": "无"
}
],
"images": []
},
{
"source": "2.1 产品场景及概要说明",
"blocks": [
{
"type": "para",
"index": 1,
"text": "产品简介用户场景说明,简单表述,让人快速看懂对用户来说这个功能是干嘛的"
},
{
"type": "para",
"index": 2,
"text": "● 产品场景说明:为了用户行车时的安全考虑,用户不可以在开车时播放视频、玩游戏"
},
{
"type": "para",
"index": 3,
"text": "● 产品可用地点: 搭载xx AUTO系统的车型座舱,海外平台化的车型"
},
{
"type": "para",
"index": 4,
"text": "● 产品使用方式:车机交互"
},
{
"type": "para",
"index": 5,
"text": "● 产品关键参数:无"
}
],
"images": []
},
{
"source": "2.2 产品架构/系统架构",
"blocks": [
{
"type": "para",
"index": 1,
"text": "系统架构"
}
],
"images": []
},
{
"source": "2.3 产品梯度配置说明",
"blocks": [
{
"type": "para",
"index": 1,
"text": "无"
}
],
"images": []
},
{
"source": "2.4 功能流程图",
"blocks": [
{
"type": "para",
"index": 1,
"text": "无"
}
],
"images": []
},
{
"source": "2.5 关键状态流转",
"blocks": [
{
"type": "para",
"index": 1,
"text": "无"
}
],
"images": []
},
{
"source": "2.6.1 硬件配置",
"blocks": [
{
"type": "para",
"index": 1,
"text": "无"
}
],
"images": []
},
{
"source": "2.7 功能列表",
"blocks": [
{
"type": "table",
"table": 1,
"headers": [
"一级功能",
"二级功能",
"三级功能",
"功能描述"
],
"rows": [
{
"columns": [
{
"name": "一级功能",
"row": 1,
"col": 1,
"text": "国内行车娱乐限制"
},
{
"name": "二级功能",
"row": 1,
"col": 2,
"text": "系统行车娱乐限制"
},
{
"name": "三级功能",
"row": 1,
"col": 3,
"text": "行车娱乐限制"
},
{
"name": "功能描述",
"row": 1,
"col": 4,
"text": "行车娱乐限制开关功能开启后,受限应用在满足条件后会退出/暂停应用,同时 toast提示用户"
}
]
},
{
"columns": [
{
"name": "一级功能",
"row": 2,
"col": 1,
"text": "国内行车娱乐限制"
},
{
"name": "二级功能",
"row": 2,
"col": 2,
"text": "系统行车娱乐限制"
},
{
"name": "三级功能",
"row": 2,
"col": 3,
"text": "行车娱乐禁止"
},
{
"name": "功能描述",
"row": 2,
"col": 4,
"text": "行车娱乐限制开关功能开启后,非 P 挡时框架禁止受限应用的启动,同时 toast提示用户"
}
]
},
{
"columns": [
{
"name": "一级功能",
"row": 3,
"col": 1,
"text": "国内行车娱乐限制"
},
{
"name": "二级功能",
"row": 3,
"col": 2,
"text": "系统行车娱乐限制"
},
{
"name": "三级功能",
"row": 3,
"col": 3,
"text": "行车娱乐限制SDK"
},
{
"name": "功能描述",
"row": 3,
"col": 4,
"text": "提供行车娱乐限制SDK,通过SDK的能力,业务可定义限制目标功能(视频,游戏等)"
}
]
},
{
"columns": [
{
"name": "一级功能",
"row": 4,
"col": 1,
"text": "国内行车娱乐限制"
},
{
"name": "二级功能",
"row": 4,
"col": 2,
"text": "行车娱乐限制开关"
},
{
"name": "三级功能",
"row": 4,
"col": 3,
"text": "默认开启"
},
{
"name": "功能描述",
"row": 4,
"col": 4,
"text": "开关开启,开启娱乐限制逻辑,用户启动受限应用时,框架拦截应用的启动,同时 toast提示用户"
}
]
},
{
"columns": [
{
"name": "一级功能",
"row": 5,
"col": 1,
"text": "国内行车娱乐限制"
},
{
"name": "二级功能",
"row": 5,
"col": 2,
"text": "行车娱乐限制开关"
},
{
"name": "三级功能",
"row": 5,
"col": 3,
"text": "关闭开关"
},
{
"name": "功能描述",
"row": 5,
"col": 4,
"text": "开关关闭,关闭娱乐限制逻辑,点击关闭按钮时弹出风险确认弹窗,用户阅读风险提示后选择关闭或取消;\n选择关闭后,娱乐限制功能关闭受限应用在非P挡启动时,框架不再限制和提示用户选择取消后,弹窗消失,开关保持开启,功能保持开启"
}
]
},
{
"columns": [
{
"name": "一级功能",
"row": 6,
"col": 1,
"text": "国内行车娱乐限制"
},
{
"name": "二级功能",
"row": 6,
"col": 2,
"text": "行车娱乐限制开关"
},
{
"name": "三级功能",
"row": 6,
"col": 3,
"text": "非 P 挡时开关置灰逻辑"
},
{
"name": "功能描述",
"row": 6,
"col": 4,
"text": "非 P 挡时行车娱乐限制开关置灰,须在P挡时才可以操作开关状态"
}
]
},
{
"columns": [
{
"name": "一级功能",
"row": 7,
"col": 1,
"text": "国内行车娱乐限制"
},
{
"name": "二级功能",
"row": 7,
"col": 2,
"text": "多屏场景"
},
{
"name": "三级功能",
"row": 7,
"col": 3,
"text": "多屏场景"
},
{
"name": "功能描述",
"row": 7,
"col": 4,
"text": "针对车内有多个屏幕的场景进行详细描述"
}
]
},
{
"columns": [
{
"name": "一级功能",
"row": 8,
"col": 1,
"text": "国外行车娱乐限制"
},
{
"name": "二级功能",
"row": 8,
"col": 2,
"text": "系统行车娱乐限制"
},
{
"name": "三级功能",
"row": 8,
"col": 3,
"text": "行车娱乐限制"
},
{
"name": "功能描述",
"row": 8,
"col": 4,
"text": "受限应用在满足条件后会退出/暂停应用,同时 toast提示用户"
}
]
},
{
"columns": [
{
"name": "一级功能",
"row": 9,
"col": 1,
"text": "国外行车娱乐限制"
},
{
"name": "二级功能",
"row": 9,
"col": 2,
"text": "系统行车娱乐限制"
},
{
"name": "三级功能",
"row": 9,
"col": 3,
"text": "行车娱乐禁止"
},
{
"name": "功能描述",
"row": 9,
"col": 4,
"text": "非 P 挡或者车速>0时框架禁止受限应用的启动,同时 toast提示用户"
}
]
},
{
"columns": [
{
"name": "一级功能",
"row": 10,
"col": 1,
"text": "国外行车娱乐限制"
},
{
"name": "二级功能",
"row": 10,
"col": 2,
"text": "多屏场景"
},
{
"name": "三级功能",
"row": 10,
"col": 3,
"text": "多屏场景"
},
{
"name": "功能描述",
"row": 10,
"col": 4,
"text": "针对车内有多个屏幕的场景进行详细描述"
}
]
}
]
}
],
"images": []
},
{
"source": "3.1 国内行车娱乐限制",
"blocks": [
{
"type": "para",
"index": 1,
"text": "下面的任务管理器图显示了行车娱乐限制清单:W899应用矩阵表(多屏互动)(手机互联应用由应用接入SDK自行实现行车娱乐限制功能)"
},
{
"type": "table",
"table": 1,
"headers": [
"功能",
"功能详细说明"
],
"rows": [
{
"columns": [
{
"name": "功能",
"row": 1,
"col": 1,
"text": "方案"
},
{
"name": "功能详细说明",
"row": 1,
"col": 2,
"text": "行车娱乐限制的实现有两种限制方法,分别是系统限制和SDK限制"
}
]
},
{
"columns": [
{
"name": "功能",
"row": 2,
"col": 1,
"text": "系统限制"
},
{
"name": "功能详细说明",
"row": 2,
"col": 2,
"text": "● 实现方法:本地和云端配置受行车限制应用的包名,系统针对已配置的包名,按照规则(4.1.2)限制其进程\n● 适用范围:视频类,游戏类,视频投屏类,KTV应用,浏览器等\n● 配置名单及方法桌面&系统相关配置 \n● 行车娱乐限制的应用在应用列表需显示P档标志"
}
]
},
{
"columns": [
{
"name": "功能",
"row": 3,
"col": 1,
"text": "SDK限制"
},
{
"name": "功能详细说明",
"row": 3,
"col": 2,
"text": "● 实现方法:应用接入对应的SDK,通过SDK的能力,业务可定义限制目标功能(视频,游戏等)\n● 适用范围:车辆设置,图库,语音,桌面,用户手册,Carplay、HiCar等"
}
]
}
]
},
{
"type": "para",
"index": 2,
"text": "** 国内的行车娱乐限制应用清单可以在魅族的后台进行配置,新增应用可以走云端修改,可以不用修改本地代码进行发版。(本地也有一份清单作为兜底的)"
},
{
"type": "para",
"index": 3,
"text": "下面的任务管理器图显示了行车娱乐限制清单:W899应用矩阵表(多屏互动)(手机互联应用由应用接入SDK自行实现行车娱乐限制功能)"
},
{
"type": "para",
"index": 4,
"text": "应用禁用:"
},
{
"type": "para",
"index": 5,
"text": "[[IMAGE:rId12]]"
}
],
"images": [
"rId12"
]
},
{
"source": "3.1.1 系统行车娱乐限制",
"blocks": [
{
"type": "table",
"table": 1,
"headers": [
"功能",
"功能详细说明"
],
"rows": [
{
"columns": [
{
"name": "功能",
"row": 1,
"col": 1,
"text": "开关"
},
{
"name": "功能详细说明",
"row": 1,
"col": 2,
"text": "● 该开关处于开启状态时行车娱乐限制生效,关闭时行车娱乐限制功能不生效\n● 该开关具体功能细节见4.1.3"
}
]
},
{
"columns": [
{
"name": "功能",
"row": 2,
"col": 1,
"text": "两种限制规则"
},
{
"name": "功能详细说明",
"row": 2,
"col": 2,
"text": "1.  系统限制方案:\n行车娱乐限制的应用在应用列表需显示P档标志;\n● 行车娱乐限制:\n 目标应用/功能处于前台时\n○ 打断:车速≥15km/h且持续5秒后,将目标应用/功能退至后台或暂停对应功能(Toast文案参考UE)\n■ 并发起toast提示用户在行车状态下无法使用\n● 行车娱乐禁止:\n 目标应用/功能处于后台时\n○ 限制:非P挡时,限制目标应用/功能启用\n■ toast提示用户请在P挡时使用该功能/应用(Toast文案参考UE)\n2.  应用自行限制方案:\n应用可根据业务需求,接入SDK自行实现限制的业务,可自定义Toast文案,并可以只对APP内部分功能进行行车娱乐限制"
}
]
},
{
"columns": [
{
"name": "功能",
"row": 3,
"col": 1,
"text": "相关流程图"
},
{
"name": "功能详细说明",
"row": 3,
"col": 2,
"text": "[[IMAGE:rId13]]"
}
]
},
{
"columns": [
{
"name": "功能",
"row": 4,
"col": 1,
"text": "交互图"
},
{
"name": "功能详细说明",
"row": 4,
"col": 2,
"text": "[[IMAGE:rId14]] [[IMAGE:rId15]]"
}
]
}
]
}
],
"images": [
"rId13",
"rId14",
"rId15"
]
},
{
"source": "四、非功能说明",
"blocks": [
{
"type": "para",
"index": 1,
"text": "不涉及"
}
],
"images": []
}
],
"image_sources": {
"rId12": {
"section": "3.1 国内行车娱乐限制"
},
"rId13": {
"section": "3.1.1 系统行车娱乐限制",
"table": 1,
"row": 3,
"column": 2,
"name": "功能详细说明"
},
"rId14": {
"section": "3.1.1 系统行车娱乐限制",
"table": 1,
"row": 4,
"column": 2,
"name": "功能详细说明"
},
"rId15": {
"section": "3.1.1 系统行车娱乐限制",
"table": 1,
"row": 4,
"column": 2,
"name": "功能详细说明"
}
},
"image_analysis": [
{
"type": "flowchart",
"description": "**1. 类型标签(单独一行):**\n\n**2. 文字描述:**\n该图片是一个关于“行车娱乐开关状态”下应用使用权限控制的决策流程图,采用标准流程图符号(圆角矩形表示起始/终止/处理步骤,菱形表示判断条件,平行四边形表示输入/操作动作,箭头表示流向),整体结构为自上而下的树状分支逻辑。\n\n顶层节点为蓝色圆角矩形“行车娱乐开关状态”,其向下分出两条主路径:\n- 左侧路径指向绿色圆角矩形“开启”\n- 右侧路径指向绿色圆角矩形“关闭”\n\n“关闭”路径直接连接至蓝色圆角矩形“不受限”,表示当行车娱乐功能关闭时,所有应用均不受限制。\n\n“开启”路径进一步分为三条并行子路径:\n1. **“其他应用”路径**(左侧):\n - 绿色圆角矩形“其他应用” → 蓝色圆角矩形“不受限”\n\n2. **“通过接入SDK限制的应用”路径**(中间):\n - 绿色圆角矩形“通过接入SDK限制的应用” → 形判断“是否在目标场景”\n - 若“否” → 黄色平行四边形“点击进入使用目标功能或进入对应页面” → 菱形判断“车辆是否达到限制条件:非P档”\n - 若“否” → 蓝色圆角矩形“不受限”\n - 若“是” → 绿色圆角矩形“应用启动被限制” → 蓝色圆角矩形“发起toast,文案由业务定义”\n - 若“是” → 菱形判断“车辆是否达到限制条件:车速>=15km/h,且超过5秒”\n - 若“否” → 蓝色圆角矩形“不受限”\n - 若“是” → 绿色圆角矩形“暂停功能,或退出对应页面” → 蓝色圆角矩形“发起toast,文案由业务定义”\n\n3. **“通过系统限制应用”路径**(右侧):\n - 绿色圆角矩形“通过系统限制应用” → 菱形判断“目标应用是否在前台”\n - 若“否” → 黄色平行四边形“点击应用,将其调用至前台” → 菱形判断“车辆是否达到限制条件:非P档”\n - 若“否” → 蓝色圆角矩形“不受限”\n - 若“是” → 绿色圆角矩形“应用启动被限制” → 蓝色圆角矩形“Toast提示‘请在P档是使用该功能’”\n - 若“是” → 菱形判断“车辆是否达到限制条件:车速>=15km/h,且超过5秒”\n - 若“否” → 蓝色圆角矩形“不受限”\n - 若“是” → 绿色圆角矩形“应用被打断,并调至后台” → 蓝色圆角矩形“Toast提示‘在行车状态下无法使用该应用’”\n\n所有判断节点均标注“是”或“否”分支标签;所有最终输出节点均为蓝色圆角矩形,包含具体行为(如“不受限”、“发起toast”等)及部分Toast文案说明。流程图清晰表达了不同应用类型、不同触发方式(点击进入 vs 点击调用)、不同车辆状态(车速、档位、是否前台)组合下的权限控制策略与用户反馈机制。",
"rid": "rId13",
"path": "D:\\projects\\jike\\output\\images\\image_rId13.png"
},
{
"type": "other",
"description": "该图片是一张 Windows 系统“任务管理器”界面的截图,当前选中的是左侧导航栏中的“启动应用”选项卡。界面顶部有搜索框,提示文字为“键入要搜索的名称、发布者或 PID”。主区域标题为“启动应用”,下方是一个表格,包含四列:名称、发布者、状态、启动影响。表格中列出了多个开机自启程序及其信息,包括:\n- XboxMicrosoft Corporation,已禁用,无影响)\n- 终端(Microsoft Corporation,已禁用,无影响)\n- Microsoft 365 CopilotMicrosoft Corporation,已禁用,未计量)\n- IntelGraphicsSoftwareIntel,已启用,未计量)\n- WXWork(腾讯,已启用,未计量)\n- msegde(Microsoft,已启用,未计量)\n- Feishu(字节跳动,已启用,未计量)\n- OneDriveMicrosoft,已启用,未计量)\n- LogiLDA.DLLLogitech,已启用,未计量)\n- RAVCpl64Realtek,已启用,未计量)\n- RtkAudUService64Realtek,已启用,未计量)\n- SecurityHealthSystrayMicrosoft,已启用,未计量)\n\n左侧导航栏从上至下依次为:任务管理器(标题)、进程、性能、应用历史记录、启动应用(高亮选中)、用户、详细信息、服务。整体为浅色主题的 Windows 11 风格 UI。",
"rid": "rId12",
"path": "D:\\projects\\jike\\output\\images\\image_rId12.png"
},
{
"type": "other",
"description": "该图片是一张车载中控屏幕的界面截图,显示的是车辆的360度环视影像(鸟瞰视角),中央为一辆白色汽车的俯视模型,车头朝上,车身下方及两侧有蓝色半透明区域表示传感器探测范围或辅助驾驶视野。屏幕顶部左侧有一个用户头像图标,右侧有信号、Wi-Fi、时间(11:11)等状态图标。屏幕底部为功能控制栏,从左到右依次为:车辆设置、主页、空调、电话、音量调节(当前75%)、应用菜单(九宫格图标)、媒体播放控制(播放/暂停、上一曲、下一曲)、麦克风(语音输入)、扬声器(音量调节)等图标;右下角有“百度AI生成”水印。图片上方标注“C. 语音”,下方有一行文字说明:“1,如果行车中使用语音打开受限应用,语音播报回复请在P档使用该功能”。整体为现代智能电动汽车中控UI界面,风格简洁、扁平化,以浅灰白为主色调。",
"rid": "rId15",
"path": "D:\\projects\\jike\\output\\images\\image_rId15.png"
},
{
"type": "flowchart",
"description": "2. 文字描述: \n该图片为一张包含两个独立场景的车载系统功能逻辑流程图,用于说明“行车娱乐限制”与“行车娱乐禁止”两种模式下受限应用的自动关闭或启用规则。整体采用左右分栏+上下分区结构,每部分均含“A. 应用列表”界面截图、“判断逻辑流程图”和“B. Toast提示”界面截图,并配有编号步骤说明。\n\n**第一部分:01,行车娱乐限制——已打开受限应用,在满足条件后自动关闭** \n- A. 应用列表:显示一个简化车载中控屏界面,顶部有用户头像,中央大区域显示“应用”字样,底部为状态栏(含时间、信号、音量等图标)。 \n- 判断逻辑流程图(居中): \n - 起始节点为菱形判断框:“应用是否属于被限制应用” \n - 若“否” → 指向矩形操作框:“保持不变” \n - 若“是” → 进入下一菱形判断框:“速度超过15km/h并持续5s” \n - 若“否” → 指向矩形操作框:“保持不变” \n - 若“是” → 指向右侧B图(Toast提示) \n- B. Toast提示:显示车载中控屏界面,中央为车辆俯视图及雷达扫描区域,上方弹出Toast提示框,文字为:“在行驶状态下无法使用该功能”。下方标注步骤“1,自动关闭应用,并Toast提示”。\n\n**第二部分:02,行车娱乐禁止——打开受限的应用** \n- A. 应用列表:显示完整车载应用网格界面,含“首页”“媒体中心”“蓝牙电话”“设置”“应用市场”等12个图标化应用入口;其中“应用市场”图标被红色箭头指向。 \n- B. Toast提示:同样为应用列表界面,但顶部叠加灰色半透明Toast提示框,文字为:“请在P档下使用该功能”。左侧用户头像旁有红色箭头从A图“应用市场”指向此处Toast提示位置。 \n- 下方标注步骤:“1,如果此时挂P档,点击则Toast提示,反之则正常打开”,表明该Toast仅在非P档时触发,P档时可正常使用。\n\n整图通过流程图+UI截图组合方式,清晰表达两种行车安全策略下的交互逻辑与反馈机制,属于典型的车载HMI功能逻辑流程图。",
"rid": "rId14",
"path": "D:\\projects\\jike\\output\\images\\image_rId14.png"
}
],
"resolved_conflicts": [
{
"conflict_id": 0,
"conflict_type": "condition_mismatch",
"section": "3.1.1 系统行车娱乐限制",
"resolution": "以文字为准",
"correction": "文字中描述的系统限制应用(前台)触发条件为:'车速≥15km/h 且持续 5 秒'",
"source": "文字"
},
{
"conflict_id": 1,
"conflict_type": "contradiction",
"section": "行车娱乐禁止规则",
"resolution": "以文字为准",
"correction": "非 P 挡时,限制目标应用/功能启用",
"source": "文字"
},
{
"conflict_id": 2,
"conflict_type": "contradiction",
"section": "行车娱乐限制规则",
"resolution": "以图片为准",
"correction": "自动关闭应用",
"source": "图片"
},
{
"conflict_id": 3,
"conflict_type": "condition_mismatch",
"section": "行车娱乐限制规则",
"resolution": "以文字为准",
"correction": "车速≥15km/h",
"source": "文字"
}
]
}