local-coze 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. local_coze/__init__.py +110 -0
  2. local_coze/cli/__init__.py +3 -0
  3. local_coze/cli/chat.py +126 -0
  4. local_coze/cli/cli.py +34 -0
  5. local_coze/cli/constants.py +7 -0
  6. local_coze/cli/db.py +81 -0
  7. local_coze/cli/embedding.py +193 -0
  8. local_coze/cli/image.py +162 -0
  9. local_coze/cli/knowledge.py +195 -0
  10. local_coze/cli/search.py +198 -0
  11. local_coze/cli/utils.py +41 -0
  12. local_coze/cli/video.py +191 -0
  13. local_coze/cli/video_edit.py +888 -0
  14. local_coze/cli/voice.py +351 -0
  15. local_coze/core/__init__.py +25 -0
  16. local_coze/core/client.py +253 -0
  17. local_coze/core/config.py +58 -0
  18. local_coze/core/exceptions.py +67 -0
  19. local_coze/database/__init__.py +29 -0
  20. local_coze/database/client.py +170 -0
  21. local_coze/database/migration.py +342 -0
  22. local_coze/embedding/__init__.py +31 -0
  23. local_coze/embedding/client.py +350 -0
  24. local_coze/embedding/models.py +130 -0
  25. local_coze/image/__init__.py +19 -0
  26. local_coze/image/client.py +110 -0
  27. local_coze/image/models.py +163 -0
  28. local_coze/knowledge/__init__.py +19 -0
  29. local_coze/knowledge/client.py +148 -0
  30. local_coze/knowledge/models.py +45 -0
  31. local_coze/llm/__init__.py +25 -0
  32. local_coze/llm/client.py +317 -0
  33. local_coze/llm/models.py +48 -0
  34. local_coze/memory/__init__.py +14 -0
  35. local_coze/memory/client.py +176 -0
  36. local_coze/s3/__init__.py +12 -0
  37. local_coze/s3/client.py +580 -0
  38. local_coze/s3/models.py +18 -0
  39. local_coze/search/__init__.py +19 -0
  40. local_coze/search/client.py +183 -0
  41. local_coze/search/models.py +57 -0
  42. local_coze/video/__init__.py +17 -0
  43. local_coze/video/client.py +347 -0
  44. local_coze/video/models.py +39 -0
  45. local_coze/video_edit/__init__.py +23 -0
  46. local_coze/video_edit/examples.py +340 -0
  47. local_coze/video_edit/frame_extractor.py +176 -0
  48. local_coze/video_edit/models.py +362 -0
  49. local_coze/video_edit/video_edit.py +631 -0
  50. local_coze/voice/__init__.py +17 -0
  51. local_coze/voice/asr.py +82 -0
  52. local_coze/voice/models.py +86 -0
  53. local_coze/voice/tts.py +94 -0
  54. local_coze-0.0.1.dist-info/METADATA +636 -0
  55. local_coze-0.0.1.dist-info/RECORD +58 -0
  56. local_coze-0.0.1.dist-info/WHEEL +4 -0
  57. local_coze-0.0.1.dist-info/entry_points.txt +3 -0
  58. local_coze-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,162 @@
1
+ import base64
2
+ import os
3
+ import time
4
+ from typing import Optional
5
+
6
+ import click
7
+ from coze_coding_utils.runtime_ctx.context import new_context
8
+ from rich.console import Console
9
+
10
+ from ..core.config import Config
11
+ from ..image import ImageGenerationClient
12
+ from .constants import RUN_MODE_HEADER, RUN_MODE_TEST
13
+
14
+ console = Console()
15
+
16
+
17
+ def validate_and_normalize_size(size: str) -> str:
18
+ if size in ["2K", "4K"]:
19
+ return size
20
+
21
+ try:
22
+ width, height = size.split("x")
23
+ w, h = int(width), int(height)
24
+
25
+ if 2560 <= w <= 4096 and 1440 <= h <= 4096:
26
+ return size
27
+ else:
28
+ console.print(
29
+ f"[yellow]Warning: Size {size} is out of range [2560x1440, 4096x4096], using default 2K[/yellow]"
30
+ )
31
+ return "2K"
32
+ except (ValueError, AttributeError):
33
+ console.print(
34
+ f"[yellow]Warning: Invalid size format {size}, using default 2K[/yellow]"
35
+ )
36
+ return "2K"
37
+
38
+
39
+ @click.command()
40
+ @click.option("--prompt", "-p", required=True, help="Text description of the image")
41
+ @click.option(
42
+ "--output", "-o", type=click.Path(), help="Output file path"
43
+ )
44
+ @click.option(
45
+ "--size",
46
+ "-s",
47
+ default="2K",
48
+ help="Image size (2K, 4K, or WIDTHxHEIGHT in range [2560x1440, 4096x4096])",
49
+ )
50
+ @click.option(
51
+ "--image", "-i", help="Reference image URL or path (for image-to-image generation)"
52
+ )
53
+ @click.option(
54
+ "--images",
55
+ multiple=True,
56
+ help="Multiple reference image URLs or paths (can be used multiple times)",
57
+ )
58
+ @click.option("--mock", is_flag=True, help="使用 mock 模式(测试运行)")
59
+ @click.option(
60
+ "--header",
61
+ "-H",
62
+ multiple=True,
63
+ help="自定义 HTTP 请求头 (格式: 'Key: Value' 或 'Key=Value',可多次使用)",
64
+ )
65
+ @click.option("--verbose", "-v", is_flag=True, help="显示详细的 HTTP 请求日志")
66
+ def image(
67
+ prompt: str,
68
+ output: str,
69
+ size: str,
70
+ image: Optional[str],
71
+ images: tuple,
72
+ mock: bool,
73
+ header: tuple,
74
+ verbose: bool,
75
+ ):
76
+ """Generate image using AI."""
77
+ try:
78
+ from .utils import parse_headers
79
+
80
+ config = Config()
81
+
82
+ ctx = None
83
+ custom_headers = parse_headers(header) or {}
84
+
85
+ if mock:
86
+ ctx = new_context(method="image.generate", headers=custom_headers)
87
+ custom_headers[RUN_MODE_HEADER] = RUN_MODE_TEST
88
+ console.print("[yellow]🧪 Mock 模式已启用(测试运行)[/yellow]")
89
+
90
+ client = ImageGenerationClient(
91
+ config, ctx=ctx, custom_headers=custom_headers, verbose=verbose
92
+ )
93
+
94
+ reference_images = None
95
+ if image:
96
+ reference_images = image
97
+ elif images:
98
+ reference_images = list(images)
99
+
100
+ validated_size = validate_and_normalize_size(size)
101
+
102
+ console.print(f"[bold cyan]Generating image...[/bold cyan]")
103
+ console.print(f"Prompt: [yellow]{prompt}[/yellow]")
104
+ console.print(f"Size: [yellow]{validated_size}[/yellow]")
105
+ if reference_images:
106
+ if isinstance(reference_images, str):
107
+ console.print(f"Reference image: [blue]{reference_images}[/blue]")
108
+ else:
109
+ console.print(
110
+ f"Reference images: [blue]{len(reference_images)} images[/blue]"
111
+ )
112
+
113
+ response = client.generate(
114
+ prompt=prompt,
115
+ size=validated_size,
116
+ image=reference_images,
117
+ response_format="b64_json",
118
+ )
119
+
120
+ if not response.data or len(response.data) == 0:
121
+ raise ValueError("No image data returned")
122
+
123
+ image_data = response.data[0]
124
+
125
+ if image_data.error:
126
+ error_msg = image_data.error.get("message", "Unknown error")
127
+ raise Exception(f"Image generation failed: {error_msg}")
128
+
129
+ if image_data.b64_json:
130
+ if output:
131
+ image_bytes = base64.b64decode(image_data.b64_json)
132
+
133
+ os.makedirs(os.path.dirname(os.path.abspath(output)), exist_ok=True)
134
+
135
+ with open(output, "wb") as f:
136
+ f.write(image_bytes)
137
+
138
+ console.print(f"[green]✓[/green] Image saved to: [bold]{output}[/bold]")
139
+ else:
140
+ console.print(f"[yellow]Note: Image generated but not saved (no output path specified)[/yellow]")
141
+ elif image_data.url:
142
+ console.print(f"\n[cyan]Complete Image URL:[/cyan]")
143
+ console.print(f"[blue]{image_data.url}[/blue]")
144
+
145
+ if output:
146
+ import requests
147
+
148
+ img_response = requests.get(image_data.url)
149
+ img_response.raise_for_status()
150
+
151
+ os.makedirs(os.path.dirname(os.path.abspath(output)), exist_ok=True)
152
+
153
+ with open(output, "wb") as f:
154
+ f.write(img_response.content)
155
+
156
+ console.print(f"[green]✓[/green] Image saved to: [bold]{output}[/bold]")
157
+ else:
158
+ raise ValueError("No image data (b64_json or url) in response")
159
+
160
+ except Exception as e:
161
+ console.print(f"[red]✗ Error: {str(e)}[/red]")
162
+ raise click.Abort()
@@ -0,0 +1,195 @@
1
+ import json
2
+ from typing import Optional
3
+
4
+ import click
5
+ from coze_coding_utils.runtime_ctx.context import new_context
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from ..core.config import Config
10
+ from ..knowledge import (
11
+ ChunkConfig,
12
+ DataSourceType,
13
+ KnowledgeClient,
14
+ KnowledgeDocument,
15
+ )
16
+ from .constants import RUN_MODE_HEADER, RUN_MODE_TEST
17
+
18
+ console = Console()
19
+
20
+
21
+ @click.group()
22
+ def knowledge():
23
+ """Knowledge Base tools."""
24
+ pass
25
+
26
+
27
+ @knowledge.command()
28
+ @click.option("--query", "-q", required=True, help="Search query")
29
+ @click.option("--dataset", "-d", multiple=True, help="Dataset names (tables) to search in")
30
+ @click.option("--top-k", "-k", default=5, help="Number of results to return")
31
+ @click.option("--min-score", "-m", default=0.0, help="Minimum similarity score")
32
+ @click.option("--mock", is_flag=True, help="Use mock mode")
33
+ @click.option(
34
+ "--header",
35
+ "-H",
36
+ multiple=True,
37
+ help="Custom HTTP headers (format: 'Key: Value')",
38
+ )
39
+ @click.option("--verbose", "-v", is_flag=True, help="Show verbose logs")
40
+ def search(
41
+ query: str,
42
+ dataset: tuple,
43
+ top_k: int,
44
+ min_score: float,
45
+ mock: bool,
46
+ header: tuple,
47
+ verbose: bool,
48
+ ):
49
+ """Search for knowledge chunks."""
50
+ try:
51
+ from .utils import parse_headers
52
+
53
+ config = Config()
54
+ ctx = None
55
+ custom_headers = parse_headers(header) or {}
56
+
57
+ if mock:
58
+ ctx = new_context(method="knowledge.search", headers=custom_headers)
59
+ custom_headers[RUN_MODE_HEADER] = RUN_MODE_TEST
60
+ console.print("[yellow]🧪 Mock mode enabled[/yellow]")
61
+
62
+ client = KnowledgeClient(
63
+ config, ctx=ctx, custom_headers=custom_headers, verbose=verbose
64
+ )
65
+
66
+ dataset_list = list(dataset) if dataset else None
67
+
68
+ response = client.search(
69
+ query=query,
70
+ table_names=dataset_list,
71
+ top_k=top_k,
72
+ min_score=min_score,
73
+ )
74
+
75
+ if response.code != 0:
76
+ console.print(f"[red]Error {response.code}: {response.msg}[/red]")
77
+ return
78
+
79
+ table = Table(title="Search Results")
80
+ table.add_column("Score", style="cyan", no_wrap=True)
81
+ table.add_column("Content", style="green")
82
+ table.add_column("Chunk ID", style="dim")
83
+ table.add_column("Doc ID", style="dim")
84
+
85
+ for chunk in response.chunks:
86
+ table.add_row(
87
+ f"{chunk.score:.4f}",
88
+ chunk.content,
89
+ chunk.chunk_id or "",
90
+ chunk.doc_id or "",
91
+ )
92
+
93
+ console.print(table)
94
+
95
+ except Exception as e:
96
+ console.print(f"[red]✗ Error: {str(e)}[/red]")
97
+ raise click.Abort()
98
+
99
+
100
+ @knowledge.command()
101
+ @click.option("--dataset", "-d", required=True, help="Dataset name (table) to add to")
102
+ @click.option("--content", "-c", multiple=True, help="Raw text content to add")
103
+ @click.option("--url", "-u", multiple=True, help="Web URL to add")
104
+ @click.option("--chunk-separator", default="\n", help="Chunk separator")
105
+ @click.option("--max-tokens", default=500, help="Max tokens per chunk")
106
+ @click.option("--remove-extra-spaces", is_flag=True, help="Normalize extra spaces")
107
+ @click.option("--remove-urls-emails", is_flag=True, help="Strip URLs and emails")
108
+ @click.option("--mock", is_flag=True, help="Use mock mode")
109
+ @click.option(
110
+ "--header",
111
+ "-H",
112
+ multiple=True,
113
+ help="Custom HTTP headers (format: 'Key: Value')",
114
+ )
115
+ @click.option("--verbose", "-v", is_flag=True, help="Show verbose logs")
116
+ def add(
117
+ dataset: str,
118
+ content: tuple,
119
+ url: tuple,
120
+ chunk_separator: str,
121
+ max_tokens: int,
122
+ remove_extra_spaces: bool,
123
+ remove_urls_emails: bool,
124
+ mock: bool,
125
+ header: tuple,
126
+ verbose: bool,
127
+ ):
128
+ """Add documents to knowledge base."""
129
+ try:
130
+ from .utils import parse_headers
131
+
132
+ config = Config()
133
+ ctx = None
134
+ custom_headers = parse_headers(header) or {}
135
+
136
+ if mock:
137
+ ctx = new_context(method="knowledge.add", headers=custom_headers)
138
+ custom_headers[RUN_MODE_HEADER] = RUN_MODE_TEST
139
+ console.print("[yellow]🧪 Mock mode enabled[/yellow]")
140
+
141
+ client = KnowledgeClient(
142
+ config, ctx=ctx, custom_headers=custom_headers, verbose=verbose
143
+ )
144
+
145
+ documents = []
146
+ for text in content:
147
+ documents.append(
148
+ KnowledgeDocument(
149
+ source=DataSourceType.TEXT,
150
+ raw_data=text,
151
+ )
152
+ )
153
+ for u in url:
154
+ documents.append(
155
+ KnowledgeDocument(
156
+ source=DataSourceType.URL,
157
+ url=u,
158
+ )
159
+ )
160
+
161
+ if not documents:
162
+ console.print("[red]No content or URL provided to add.[/red]")
163
+ return
164
+
165
+ chunk_config = ChunkConfig(
166
+ separator=chunk_separator,
167
+ max_tokens=max_tokens,
168
+ remove_extra_spaces=remove_extra_spaces,
169
+ remove_urls_emails=remove_urls_emails,
170
+ )
171
+
172
+ response = client.add_documents(
173
+ documents=documents,
174
+ table_name=dataset,
175
+ chunk_config=chunk_config,
176
+ )
177
+
178
+ if response.code != 0:
179
+ console.print(f"[red]Error {response.code}: {response.msg}[/red]")
180
+ return
181
+
182
+ table = Table(title="Add Documents Result")
183
+ table.add_column("Field", style="cyan", no_wrap=True)
184
+ table.add_column("Value", style="green")
185
+
186
+ table.add_row("Code", str(response.code))
187
+ table.add_row("Message", response.msg)
188
+ if response.doc_ids:
189
+ table.add_row("Doc IDs", ", ".join(response.doc_ids))
190
+
191
+ console.print(table)
192
+
193
+ except Exception as e:
194
+ console.print(f"[red]✗ Error: {str(e)}[/red]")
195
+ raise click.Abort()
@@ -0,0 +1,198 @@
1
+ import json
2
+ from typing import Optional
3
+ import click
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from rich.panel import Panel
7
+ from rich.markdown import Markdown
8
+
9
+ from ..search import SearchClient
10
+ from ..core.config import Config
11
+
12
+ console = Console()
13
+
14
+
15
+ def display_web_results_table(response, show_url: bool = True):
16
+ """使用 Rich 表格显示 Web 搜索结果"""
17
+ if not response.web_items:
18
+ console.print("[yellow]没有找到搜索结果[/yellow]")
19
+ return
20
+
21
+ table = Table(title="Web 搜索结果", show_header=True, header_style="bold cyan")
22
+ table.add_column("序号", style="dim", width=6)
23
+ table.add_column("标题", style="bold")
24
+ table.add_column("站点", style="cyan", width=15)
25
+ table.add_column("摘要", style="white")
26
+ if show_url:
27
+ table.add_column("URL", style="blue", width=40)
28
+
29
+ for idx, item in enumerate(response.web_items, 1):
30
+ title = item.title[:50] + "..." if len(item.title) > 50 else item.title
31
+ site = item.site_name or "未知"
32
+ snippet = item.snippet[:80] + "..." if len(item.snippet) > 80 else item.snippet
33
+
34
+ row = [str(idx), title, site, snippet]
35
+ if show_url:
36
+ url = item.url[:40] + "..." if item.url and len(item.url) > 40 else (item.url or "")
37
+ row.append(url)
38
+
39
+ table.add_row(*row)
40
+
41
+ console.print(table)
42
+
43
+ if response.summary:
44
+ console.print()
45
+ console.print(Panel(
46
+ Markdown(response.summary),
47
+ title="[bold green]AI 摘要[/bold green]",
48
+ border_style="green"
49
+ ))
50
+
51
+ console.print(f"\n[green]✓[/green] 找到 {len(response.web_items)} 条结果")
52
+
53
+
54
+ def display_image_results_table(response):
55
+ """使用 Rich 表格显示图片搜索结果"""
56
+ if not response.image_items:
57
+ console.print("[yellow]没有找到图片结果[/yellow]")
58
+ return
59
+
60
+ table = Table(title="图片搜索结果", show_header=True, header_style="bold magenta")
61
+ table.add_column("序号", style="dim", width=6)
62
+ table.add_column("标题", style="bold")
63
+ table.add_column("站点", style="cyan", width=15)
64
+ table.add_column("图片 URL", style="blue")
65
+ table.add_column("尺寸", style="yellow", width=12)
66
+
67
+ for idx, item in enumerate(response.image_items, 1):
68
+ title = item.title[:40] + "..." if item.title and len(item.title) > 40 else (item.title or "无标题")
69
+ site = item.site_name or "未知"
70
+ image_url = item.image.url[:50] + "..." if len(item.image.url) > 50 else item.image.url
71
+ size = f"{item.image.width}x{item.image.height}" if item.image.width and item.image.height else "未知"
72
+
73
+ table.add_row(str(idx), title, site, image_url, size)
74
+
75
+ console.print(table)
76
+ console.print(f"\n[green]✓[/green] 找到 {len(response.image_items)} 张图片")
77
+
78
+
79
+ def display_simple_format(response):
80
+ """简单文本格式输出"""
81
+ if response.web_items:
82
+ for idx, item in enumerate(response.web_items, 1):
83
+ console.print(f"[{idx}] {item.title} - {item.site_name or '未知'}")
84
+ if item.url:
85
+ console.print(f" {item.url}")
86
+ console.print(f" {item.snippet}")
87
+ console.print()
88
+
89
+ if response.summary:
90
+ console.print("=" * 60)
91
+ console.print("AI 摘要:")
92
+ console.print(response.summary)
93
+ console.print("=" * 60)
94
+
95
+ if response.image_items:
96
+ for idx, item in enumerate(response.image_items, 1):
97
+ console.print(f"[{idx}] {item.title or '无标题'} - {item.site_name or '未知'}")
98
+ console.print(f" 图片: {item.image.url}")
99
+ if item.image.width and item.image.height:
100
+ console.print(f" 尺寸: {item.image.width}x{item.image.height}")
101
+ console.print()
102
+
103
+
104
+ def save_to_json(response, output_path: str):
105
+ """保存结果到 JSON 文件"""
106
+ data = response.model_dump()
107
+ with open(output_path, 'w', encoding='utf-8') as f:
108
+ json.dump(data, f, ensure_ascii=False, indent=2)
109
+ console.print(f"[green]✓[/green] 结果已保存到: {output_path}")
110
+
111
+
112
+ @click.command()
113
+ @click.argument("query")
114
+ @click.option("--type", "-t", type=click.Choice(["web", "image", "web_summary"]), default="web", help="搜索类型: web(网页), image(图片), web_summary(网页+AI摘要)")
115
+ @click.option("--count", "-c", default=10, help="返回结果数量")
116
+ @click.option("--summary", "-s", is_flag=True, help="是否需要 AI 摘要 (仅 web 类型)")
117
+ @click.option("--need-content", is_flag=True, help="仅返回有正文的结果 (仅 web 类型)")
118
+ @click.option("--need-url", is_flag=True, help="仅返回有原文链接的结果 (仅 web 类型)")
119
+ @click.option("--sites", help="指定搜索的站点范围,逗号分隔 (仅 web 类型)")
120
+ @click.option("--block-hosts", help="屏蔽的站点,逗号分隔 (仅 web 类型)")
121
+ @click.option("--time-range", help="发文时间范围,如: 1d, 1w, 1m (仅 web 类型)")
122
+ @click.option("--output", "-o", type=click.Path(), help="输出 JSON 文件路径")
123
+ @click.option("--format", "-f", type=click.Choice(["table", "json", "simple"]), default="table", help="输出格式")
124
+ @click.option(
125
+ "--header",
126
+ "-H",
127
+ multiple=True,
128
+ help="自定义 HTTP 请求头 (格式: 'Key: Value' 或 'Key=Value',可多次使用)",
129
+ )
130
+ @click.option("--verbose", "-v", is_flag=True, help="显示详细的 HTTP 请求日志")
131
+ def search(query, type, count, summary, need_content, need_url, sites, block_hosts, time_range, output, format, header, verbose):
132
+ """联网搜索
133
+
134
+ 支持三种搜索类型:
135
+ - web: 普通网页搜索 (默认)
136
+ - image: 图片搜索
137
+ - web_summary: 网页搜索 + AI 智能摘要
138
+
139
+ 示例:
140
+ # 网页搜索
141
+ coze-coding-ai search "AI 最新进展"
142
+ coze-coding-ai search "AI 最新进展" --type web --count 20 --summary
143
+
144
+ # 图片搜索
145
+ coze-coding-ai search "可爱的猫咪" --type image
146
+ coze-coding-ai search "可爱的猫咪" -t image -c 20
147
+
148
+ # 网页搜索 + AI 摘要
149
+ coze-coding-ai search "量子计算原理" --type web_summary
150
+ coze-coding-ai search "量子计算原理" -t web_summary -c 15
151
+
152
+ # 高级过滤
153
+ coze-coding-ai search "Python 教程" --sites "python.org,github.com" --need-content
154
+ coze-coding-ai search "新闻" --time-range "1d" --need-url
155
+ """
156
+ try:
157
+ from .utils import parse_headers
158
+
159
+ config = Config()
160
+ custom_headers = parse_headers(header)
161
+ client = SearchClient(config, custom_headers=custom_headers, verbose=verbose)
162
+
163
+ if type == "image":
164
+ console.print(f"[bold magenta]正在搜索图片:[/bold magenta] {query}")
165
+ response = client.image_search(query=query, count=count)
166
+ elif type == "web_summary":
167
+ console.print(f"[bold cyan]正在搜索并生成摘要:[/bold cyan] {query}")
168
+ response = client.web_search_with_summary(query=query, count=count)
169
+ else:
170
+ console.print(f"[bold cyan]正在搜索:[/bold cyan] {query}")
171
+ response = client.search(
172
+ query=query,
173
+ search_type="web",
174
+ count=count,
175
+ need_content=need_content,
176
+ need_url=need_url,
177
+ sites=sites,
178
+ block_hosts=block_hosts,
179
+ need_summary=summary,
180
+ time_range=time_range
181
+ )
182
+
183
+ if format == "json":
184
+ console.print_json(data=response.model_dump())
185
+ elif format == "simple":
186
+ display_simple_format(response)
187
+ else:
188
+ if type == "image":
189
+ display_image_results_table(response)
190
+ else:
191
+ display_web_results_table(response, show_url=True)
192
+
193
+ if output:
194
+ save_to_json(response, output)
195
+
196
+ except Exception as e:
197
+ console.print(f"[red]✗ 错误: {str(e)}[/red]")
198
+ raise click.Abort()
@@ -0,0 +1,41 @@
1
+ from typing import Dict, Optional, Tuple
2
+
3
+
4
+ def parse_headers(header_strings: Tuple[str, ...]) -> Optional[Dict[str, str]]:
5
+ """
6
+ 解析命令行传入的 header 字符串
7
+
8
+ 支持格式:
9
+ - "Key: Value"
10
+ - "Key=Value"
11
+
12
+ 示例:
13
+ --header "X-Custom: value1" --header "X-Test=value2"
14
+
15
+ Args:
16
+ header_strings: 命令行传入的 header 字符串元组
17
+
18
+ Returns:
19
+ Dict[str, str] or None: 解析后的 headers 字典,如果没有则返回 None
20
+
21
+ Raises:
22
+ ValueError: 如果 header 格式不正确
23
+ """
24
+ if not header_strings:
25
+ return None
26
+
27
+ headers = {}
28
+ for header_str in header_strings:
29
+ if ": " in header_str:
30
+ key, value = header_str.split(": ", 1)
31
+ elif "=" in header_str:
32
+ key, value = header_str.split("=", 1)
33
+ else:
34
+ raise ValueError(
35
+ f"Invalid header format: '{header_str}'. "
36
+ f"Use 'Key: Value' or 'Key=Value'"
37
+ )
38
+
39
+ headers[key.strip()] = value.strip()
40
+
41
+ return headers if headers else None