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.
- local_coze/__init__.py +110 -0
- local_coze/cli/__init__.py +3 -0
- local_coze/cli/chat.py +126 -0
- local_coze/cli/cli.py +34 -0
- local_coze/cli/constants.py +7 -0
- local_coze/cli/db.py +81 -0
- local_coze/cli/embedding.py +193 -0
- local_coze/cli/image.py +162 -0
- local_coze/cli/knowledge.py +195 -0
- local_coze/cli/search.py +198 -0
- local_coze/cli/utils.py +41 -0
- local_coze/cli/video.py +191 -0
- local_coze/cli/video_edit.py +888 -0
- local_coze/cli/voice.py +351 -0
- local_coze/core/__init__.py +25 -0
- local_coze/core/client.py +253 -0
- local_coze/core/config.py +58 -0
- local_coze/core/exceptions.py +67 -0
- local_coze/database/__init__.py +29 -0
- local_coze/database/client.py +170 -0
- local_coze/database/migration.py +342 -0
- local_coze/embedding/__init__.py +31 -0
- local_coze/embedding/client.py +350 -0
- local_coze/embedding/models.py +130 -0
- local_coze/image/__init__.py +19 -0
- local_coze/image/client.py +110 -0
- local_coze/image/models.py +163 -0
- local_coze/knowledge/__init__.py +19 -0
- local_coze/knowledge/client.py +148 -0
- local_coze/knowledge/models.py +45 -0
- local_coze/llm/__init__.py +25 -0
- local_coze/llm/client.py +317 -0
- local_coze/llm/models.py +48 -0
- local_coze/memory/__init__.py +14 -0
- local_coze/memory/client.py +176 -0
- local_coze/s3/__init__.py +12 -0
- local_coze/s3/client.py +580 -0
- local_coze/s3/models.py +18 -0
- local_coze/search/__init__.py +19 -0
- local_coze/search/client.py +183 -0
- local_coze/search/models.py +57 -0
- local_coze/video/__init__.py +17 -0
- local_coze/video/client.py +347 -0
- local_coze/video/models.py +39 -0
- local_coze/video_edit/__init__.py +23 -0
- local_coze/video_edit/examples.py +340 -0
- local_coze/video_edit/frame_extractor.py +176 -0
- local_coze/video_edit/models.py +362 -0
- local_coze/video_edit/video_edit.py +631 -0
- local_coze/voice/__init__.py +17 -0
- local_coze/voice/asr.py +82 -0
- local_coze/voice/models.py +86 -0
- local_coze/voice/tts.py +94 -0
- local_coze-0.0.1.dist-info/METADATA +636 -0
- local_coze-0.0.1.dist-info/RECORD +58 -0
- local_coze-0.0.1.dist-info/WHEEL +4 -0
- local_coze-0.0.1.dist-info/entry_points.txt +3 -0
- local_coze-0.0.1.dist-info/licenses/LICENSE +21 -0
local_coze/cli/image.py
ADDED
|
@@ -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()
|
local_coze/cli/search.py
ADDED
|
@@ -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()
|
local_coze/cli/utils.py
ADDED
|
@@ -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
|